diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index f252a96d2f..b539629874 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -1053,12 +1053,15 @@
"how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?",
"choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.",
"setting_up_zapier": "Setting up your Zapier integration",
+ "setting_up_make": "Setting up your Make integration",
"generate_api_key": "Generate API key",
"generate_api_key_description": "Generate an API key to use with {{appName}} at",
"your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.1><2>Choose your account and then enter your Unique API Key.2><3>Test your Trigger.3><4>You're set!4>",
+ "make_setup_instructions": "<0>Log into your Make account and create a new Scenario.0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.1><2>Choose your account and then enter your Unique API Key.2><3>Test your Trigger.3><4>You're set!4>",
"install_zapier_app": "Please first install the Zapier App in the app store.",
+ "install_make_app": "Please first install the Make App in the app store.",
"connect_apple_server": "Connect to Apple Server",
"calendar_url": "Calendar URL",
"apple_server_generate_password": "Generate an app specific password to use with {{appName}} at",
@@ -1089,6 +1092,7 @@
"or_lowercase": "or",
"nevermind": "Nevermind",
"go_to": "Go to: ",
+ "invite_link_make": "
Go toMake Invite Link and install Cal.com appp ",
"zapier_invite_link": "Zapier Invite Link",
"meeting_url_provided_after_confirmed": "A Meeting URL will be created once the event is confirmed.",
"dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee",
diff --git a/packages/app-store/_pages/setup/_getStaticProps.tsx b/packages/app-store/_pages/setup/_getStaticProps.tsx
index 5e1f963ef6..3a10131c9d 100644
--- a/packages/app-store/_pages/setup/_getStaticProps.tsx
+++ b/packages/app-store/_pages/setup/_getStaticProps.tsx
@@ -2,6 +2,7 @@ import type { GetStaticPropsContext } from "next";
export const AppSetupPageMap = {
zapier: import("../../zapier/pages/setup/_getStaticProps"),
+ make: import("../../make/pages/setup/_getStaticProps"),
};
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
diff --git a/packages/app-store/_pages/setup/index.tsx b/packages/app-store/_pages/setup/index.tsx
index 22331872a6..6438b04a29 100644
--- a/packages/app-store/_pages/setup/index.tsx
+++ b/packages/app-store/_pages/setup/index.tsx
@@ -9,6 +9,7 @@ export const AppSetupMap = {
"exchange2016-calendar": dynamic(() => import("../../exchange2016calendar/pages/setup")),
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
zapier: dynamic(() => import("../../zapier/pages/setup")),
+ make: dynamic(() => import("../../make/pages/setup")),
closecom: dynamic(() => import("../../closecom/pages/setup")),
sendgrid: dynamic(() => import("../../sendgrid/pages/setup")),
paypal: dynamic(() => import("../../paypal/pages/setup")),
diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts
index 019da1d5b9..b3b679d8ed 100644
--- a/packages/app-store/apps.keys-schemas.generated.ts
+++ b/packages/app-store/apps.keys-schemas.generated.ts
@@ -12,6 +12,7 @@ import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
+import { appKeysSchema as make_zod_ts } from "./make/zod";
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
@@ -43,6 +44,7 @@ export const appKeysSchemas = {
hubspot: hubspot_zod_ts,
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
+ make: make_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,
diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts
index 3b7308219e..c9c05fa170 100644
--- a/packages/app-store/apps.metadata.generated.ts
+++ b/packages/app-store/apps.metadata.generated.ts
@@ -29,6 +29,7 @@ import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
+import make_config_json from "./make/config.json";
import metapixel_config_json from "./metapixel/config.json";
import mirotalk_config_json from "./mirotalk/config.json";
import n8n_config_json from "./n8n/config.json";
@@ -99,6 +100,7 @@ export const appStoreMetadata = {
huddle01video: huddle01video__metadata_ts,
jitsivideo: jitsivideo__metadata_ts,
larkcalendar: larkcalendar__metadata_ts,
+ make: make_config_json,
metapixel: metapixel_config_json,
mirotalk: mirotalk_config_json,
n8n: n8n_config_json,
diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts
index 49ff7731ec..1801493d85 100644
--- a/packages/app-store/apps.schemas.generated.ts
+++ b/packages/app-store/apps.schemas.generated.ts
@@ -12,6 +12,7 @@ import { appDataSchema as gtm_zod_ts } from "./gtm/zod";
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
+import { appDataSchema as make_zod_ts } from "./make/zod";
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
@@ -43,6 +44,7 @@ export const appDataSchemas = {
hubspot: hubspot_zod_ts,
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
+ make: make_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,
diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts
index 6eb15be4f0..e37b6e7113 100644
--- a/packages/app-store/apps.server.generated.ts
+++ b/packages/app-store/apps.server.generated.ts
@@ -29,6 +29,7 @@ export const apiHandlers = {
huddle01video: import("./huddle01video/api"),
jitsivideo: import("./jitsivideo/api"),
larkcalendar: import("./larkcalendar/api"),
+ make: import("./make/api"),
metapixel: import("./metapixel/api"),
mirotalk: import("./mirotalk/api"),
n8n: import("./n8n/api"),
diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts
index 8982003f00..10046d5393 100644
--- a/packages/app-store/index.ts
+++ b/packages/app-store/index.ts
@@ -26,6 +26,7 @@ const appStore = {
webexvideo: () => import("./webex"),
giphy: () => import("./giphy"),
zapier: () => import("./zapier"),
+ make: () => import("./make"),
exchange2013calendar: () => import("./exchange2013calendar"),
exchange2016calendar: () => import("./exchange2016calendar"),
exchangecalendar: () => import("./exchangecalendar"),
diff --git a/packages/app-store/make/DESCRIPTION.md b/packages/app-store/make/DESCRIPTION.md
new file mode 100644
index 0000000000..ff99243ef5
--- /dev/null
+++ b/packages/app-store/make/DESCRIPTION.md
@@ -0,0 +1,10 @@
+---
+items:
+ - 1.jpeg
+ - 2.jpeg
+ - 3.jpeg
+ - 4.jpeg
+ - 5.jpeg
+---
+
+Workflow automation for everyone. Use the Cal.com app in Make to automate your workflows when a booking is created, rescheduled, cancelled or when a meeting has ended. You can also get all your booking with the 'List Bookings' module. **After Installation:** Have you lost your API key? You can always generate a new key on the **Make Setup Page **
\ No newline at end of file
diff --git a/packages/app-store/make/README.md b/packages/app-store/make/README.md
new file mode 100644
index 0000000000..e3de1d11c7
--- /dev/null
+++ b/packages/app-store/make/README.md
@@ -0,0 +1,22 @@
+# Setting up Make Integration
+
+
+1. Install the app from the Cal app store and generate an API key. Copy the API key.
+2. Go to `/admin/apps/automation` in Cal and set the `invite_link` for Make to `https://www.make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510` to use the app.
+3. Create a [Make account](https://www.make.com/en/login), if you don't have one.
+4. Go to `Scenarios` in the sidebar and click on **Create a new scenario**.
+5. Search for `Cal.com` in the apps list and select from the list of triggers - Booking Created, Booking Deleted, Booking Rescheduled, Meeting Ended
+6. To create a **connection** you will need your Cal deployment url and the app API Key generated above. You only need to create a **connection** once, all webhooks can use that connection.
+7. Setup the webhook for the desired event in Make.
+8. To delete a webhook, go to `Webhooks` in the left sidebar in Make, pick the webhook you want to delete and click **delete**.
+
+## Localhost or Self-hosting
+
+Localhost urls can not be used as the base URL for api endpoints
+
+Possible solution: using [https://ngrok.com/](https://ngrok.com/)
+
+1. Create Account
+2. [Download](https://ngrok.com/download) ngrok and start a tunnel to your running localhost
+ - Use forwarding url as your baseUrl for the URL endpoints
+3. Use the ngrok url as your Cal deployment url when creating the **Connection** in Make.
\ No newline at end of file
diff --git a/packages/app-store/make/api/add.ts b/packages/app-store/make/api/add.ts
new file mode 100644
index 0000000000..ac5e8078c3
--- /dev/null
+++ b/packages/app-store/make/api/add.ts
@@ -0,0 +1,20 @@
+import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
+import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
+
+import appConfig from "../config.json";
+
+const handler: AppDeclarativeHandler = {
+ appType: appConfig.type,
+ variant: appConfig.variant,
+ slug: appConfig.slug,
+ supportsMultipleInstalls: false,
+ handlerType: "add",
+ redirect: {
+ newTab: true,
+ url: "/apps/make/setup",
+ },
+ createCredential: ({ appType, user, slug, teamId }) =>
+ createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
+};
+
+export default handler;
diff --git a/packages/app-store/make/api/index.ts b/packages/app-store/make/api/index.ts
new file mode 100644
index 0000000000..d32a6d07b0
--- /dev/null
+++ b/packages/app-store/make/api/index.ts
@@ -0,0 +1,5 @@
+export { default as add } from "./add";
+export { default as listBookings } from "./subscriptions/listBookings";
+export { default as deleteSubscription } from "./subscriptions/deleteSubscription";
+export { default as addSubscription } from "./subscriptions/addSubscription";
+export { default as me } from "./subscriptions/me";
diff --git a/packages/app-store/make/api/subscriptions/addSubscription.ts b/packages/app-store/make/api/subscriptions/addSubscription.ts
new file mode 100644
index 0000000000..3bfd90da16
--- /dev/null
+++ b/packages/app-store/make/api/subscriptions/addSubscription.ts
@@ -0,0 +1,38 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
+import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
+import { defaultHandler, defaultResponder } from "@calcom/lib/server";
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const apiKey = req.query.apiKey as string;
+
+ if (!apiKey) {
+ return res.status(401).json({ message: "No API key provided" });
+ }
+
+ const validKey = await findValidApiKey(apiKey, "make");
+
+ if (!validKey) {
+ return res.status(401).json({ message: "API key not valid" });
+ }
+
+ const { subscriberUrl, triggerEvent } = req.body;
+
+ const createAppSubscription = await addSubscription({
+ appApiKey: validKey,
+ triggerEvent: triggerEvent,
+ subscriberUrl: subscriberUrl,
+ appId: "make",
+ });
+
+ if (!createAppSubscription) {
+ return res.status(500).json({ message: "Could not create subscription." });
+ }
+
+ res.status(200).json(createAppSubscription);
+}
+
+export default defaultHandler({
+ POST: Promise.resolve({ default: defaultResponder(handler) }),
+});
diff --git a/packages/app-store/make/api/subscriptions/deleteSubscription.ts b/packages/app-store/make/api/subscriptions/deleteSubscription.ts
new file mode 100644
index 0000000000..20b55e7714
--- /dev/null
+++ b/packages/app-store/make/api/subscriptions/deleteSubscription.ts
@@ -0,0 +1,40 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import z from "zod";
+
+import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
+import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
+import { defaultHandler, defaultResponder } from "@calcom/lib/server";
+
+const querySchema = z.object({
+ apiKey: z.string(),
+ id: z.string(),
+});
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { apiKey, id } = querySchema.parse(req.query);
+
+ if (!apiKey) {
+ return res.status(401).json({ message: "No API key provided" });
+ }
+
+ const validKey = await findValidApiKey(apiKey, "make");
+
+ if (!validKey) {
+ return res.status(401).json({ message: "API key not valid" });
+ }
+
+ const deleteEventSubscription = await deleteSubscription({
+ appApiKey: validKey,
+ webhookId: id,
+ appId: "make",
+ });
+
+ if (!deleteEventSubscription) {
+ return res.status(500).json({ message: "Could not delete subscription." });
+ }
+ res.status(204).json({ message: "Subscription is deleted." });
+}
+
+export default defaultHandler({
+ DELETE: Promise.resolve({ default: defaultResponder(handler) }),
+});
diff --git a/packages/app-store/make/api/subscriptions/listBookings.ts b/packages/app-store/make/api/subscriptions/listBookings.ts
new file mode 100644
index 0000000000..b4204fbe94
--- /dev/null
+++ b/packages/app-store/make/api/subscriptions/listBookings.ts
@@ -0,0 +1,35 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
+import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger";
+import { defaultHandler, defaultResponder } from "@calcom/lib/server";
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const apiKey = req.query.apiKey as string;
+
+ if (!apiKey) {
+ return res.status(401).json({ message: "No API key provided" });
+ }
+
+ const validKey = await findValidApiKey(apiKey, "make");
+
+ if (!validKey) {
+ return res.status(401).json({ message: "API key not valid" });
+ }
+ const bookings = await listBookings(validKey);
+
+ if (!bookings) {
+ return res.status(500).json({ message: "Unable to get bookings." });
+ }
+ if (bookings.length === 0) {
+ const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
+ return res.status(404).json({
+ message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
+ });
+ }
+ res.status(201).json(bookings);
+}
+
+export default defaultHandler({
+ GET: Promise.resolve({ default: defaultResponder(handler) }),
+});
diff --git a/packages/app-store/make/api/subscriptions/me.ts b/packages/app-store/make/api/subscriptions/me.ts
new file mode 100644
index 0000000000..f02a84c9d2
--- /dev/null
+++ b/packages/app-store/make/api/subscriptions/me.ts
@@ -0,0 +1,35 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
+import prisma from "@calcom/prisma";
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const apiKey = req.query.apiKey as string;
+
+ if (!apiKey) {
+ return res.status(401).json({ message: "No API key provided" });
+ }
+
+ const validKey = await findValidApiKey(apiKey, "make");
+
+ if (!validKey) {
+ return res.status(401).json({ message: "API key not valid" });
+ }
+
+ if (req.method === "GET") {
+ try {
+ const user = await prisma.user.findFirst({
+ where: {
+ id: validKey.userId,
+ },
+ select: {
+ username: true,
+ },
+ });
+ res.status(201).json(user);
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ message: "Unable to get User." });
+ }
+ }
+}
diff --git a/packages/app-store/make/config.json b/packages/app-store/make/config.json
new file mode 100644
index 0000000000..b88840f47d
--- /dev/null
+++ b/packages/app-store/make/config.json
@@ -0,0 +1,18 @@
+{
+ "/*": "Don't modify slug - If required, do it using cli edit command",
+ "name": "Make",
+ "slug": "make",
+ "type": "make_automation",
+ "logo": "icon.svg",
+ "url": "https://cal.com/apps/make",
+ "variant": "automation",
+ "categories": ["automation"],
+ "publisher": "aa2dee2",
+ "email": "support@cal.com",
+ "description": "Automate workflows",
+ "isTemplate": false,
+ "__createdUsingCli": true,
+ "__template": "basic",
+ "imageSrc": "icon.svg",
+ "dirName": "make"
+}
diff --git a/packages/app-store/make/index.ts b/packages/app-store/make/index.ts
new file mode 100644
index 0000000000..d7f3602204
--- /dev/null
+++ b/packages/app-store/make/index.ts
@@ -0,0 +1 @@
+export * as api from "./api";
diff --git a/packages/app-store/make/package.json b/packages/app-store/make/package.json
new file mode 100644
index 0000000000..ce3fc5fa9b
--- /dev/null
+++ b/packages/app-store/make/package.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "private": true,
+ "name": "@calcom/make",
+ "version": "0.0.0",
+ "main": "./index.ts",
+ "description": "Workflow automation for everyone. Use the Cal.com Make app to trigger your workflows when a booking is created, rescheduled, or cancelled, or after a meeting ends.",
+ "dependencies": {
+ "@calcom/lib": "*"
+ },
+ "devDependencies": {
+ "@calcom/types": "*",
+ "@types/node-schedule": "^2.1.0"
+ }
+}
diff --git a/packages/app-store/make/pages/setup/_getStaticProps.tsx b/packages/app-store/make/pages/setup/_getStaticProps.tsx
new file mode 100644
index 0000000000..1c6cac0efe
--- /dev/null
+++ b/packages/app-store/make/pages/setup/_getStaticProps.tsx
@@ -0,0 +1,20 @@
+import type { GetStaticPropsContext } from "next";
+
+import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug";
+
+export interface IMakeSetupProps {
+ inviteLink: string;
+}
+
+export const getStaticProps = async (ctx: GetStaticPropsContext) => {
+ if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
+ let inviteLink = "";
+ const appKeys = await getAppKeysFromSlug("make");
+ if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link;
+
+ return {
+ props: {
+ inviteLink,
+ },
+ };
+};
diff --git a/packages/app-store/make/pages/setup/index.tsx b/packages/app-store/make/pages/setup/index.tsx
new file mode 100644
index 0000000000..7f097dd14e
--- /dev/null
+++ b/packages/app-store/make/pages/setup/index.tsx
@@ -0,0 +1,178 @@
+import type { InferGetStaticPropsType } from "next";
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import { useState } from "react";
+import { Toaster } from "react-hot-toast";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Button, Tooltip, showToast } from "@calcom/ui";
+import { Clipboard } from "@calcom/ui/components/icon";
+
+import type { getStaticProps } from "./_getStaticProps";
+
+const MAKE = "make";
+
+export default function MakeSetup({ inviteLink }: InferGetStaticPropsType) {
+ const [newApiKeys, setNewApiKeys] = useState>({});
+
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const integrations = trpc.viewer.integrations.useQuery({ variant: "automation" });
+ const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: MAKE });
+ const teamsList = trpc.viewer.teams.listOwnedTeams.useQuery(undefined, {
+ refetchOnWindowFocus: false,
+ });
+ const teams = teamsList.data?.map((team) => ({ id: team.id, name: team.name }));
+ const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation({
+ onSuccess: () => {
+ utils.viewer.apiKeys.findKeyOfType.invalidate();
+ },
+ });
+ const makeCredentials: { userCredentialIds: number[] } | undefined = integrations.data?.items.find(
+ (item: { type: string }) => item.type === "make_automation"
+ );
+ const [credentialId] = makeCredentials?.userCredentialIds || [false];
+ const showContent = integrations.data && integrations.isSuccess && credentialId;
+
+ async function createApiKey(teamId?: number) {
+ const event = { note: "Make", expiresAt: null, appId: MAKE, teamId };
+ const apiKey = await utils.client.viewer.apiKeys.create.mutate(event);
+
+ if (oldApiKey.data) {
+ const oldKey = teamId
+ ? oldApiKey.data.find((key) => key.teamId === teamId)
+ : oldApiKey.data.find((key) => !key.teamId);
+
+ if (oldKey) {
+ deleteApiKey.mutate({
+ id: oldKey.id,
+ });
+ }
+ }
+
+ return apiKey;
+ }
+
+ async function generateApiKey(teamId?: number) {
+ const apiKey = await createApiKey(teamId);
+ setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
+ }
+
+ if (integrations.isLoading) {
+ return
;
+ }
+
+ return (
+
+ {showContent ? (
+
+
+
+
+
+
+
{t("setting_up_make")}
+
+ <>
+
{t("generate_api_key")}:
+ {!teams ? (
+
createApiKey()} className="mb-4 mt-2">
+ {t("generate_api_key")}
+
+ ) : (
+ <>
+
Your event types:
+ {!newApiKeys[""] ? (
+
generateApiKey()} className="mb-4 mt-2">
+ {t("generate_api_key")}
+
+ ) : (
+
+ )}
+ {teams.map((team) => {
+ return (
+
+
{team.name}:
+ {!newApiKeys[team.id] ? (
+
generateApiKey(team.id)}
+ className="mb-4 mt-2">
+ {t("generate_api_key")}
+
+ ) : (
+
+ )}
+
+ );
+ })}
+ >
+ )}
+ >
+
+
+
+
+ Go to
+
+ Make Invite Link
+
+ and install the Cal.com app.
+
+
+
+
+ Log into your Make account and create a new Scenario.
+ Select Cal.com as your Trigger app. Also choose a Trigger event.
+ Choose your account and then enter your Unique API Key.
+ Test your Trigger.
+ You're set!
+
+
+
+
{t("done")}
+
+
+
+
+ ) : (
+
+
{t("install_make_app")}
+
+
+ {t("go_to_app_store")}
+
+
+
+ )}
+
+
+ );
+}
+
+const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
+ const { t } = useLocale();
+ return (
+
+
+
+ {apiKey}
+
+
+ {
+ navigator.clipboard.writeText(apiKey);
+ showToast(t("api_key_copied"), "success");
+ }}
+ type="button"
+ className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
+
+ {t("copy")}
+
+
+
+
{t("copy_somewhere_safe")}
+
+ );
+};
diff --git a/packages/app-store/make/static/1.jpeg b/packages/app-store/make/static/1.jpeg
new file mode 100644
index 0000000000..54f811f918
Binary files /dev/null and b/packages/app-store/make/static/1.jpeg differ
diff --git a/packages/app-store/make/static/2.jpeg b/packages/app-store/make/static/2.jpeg
new file mode 100644
index 0000000000..c6637a01c8
Binary files /dev/null and b/packages/app-store/make/static/2.jpeg differ
diff --git a/packages/app-store/make/static/3.jpeg b/packages/app-store/make/static/3.jpeg
new file mode 100644
index 0000000000..15c25d425f
Binary files /dev/null and b/packages/app-store/make/static/3.jpeg differ
diff --git a/packages/app-store/make/static/4.jpeg b/packages/app-store/make/static/4.jpeg
new file mode 100644
index 0000000000..6ae8a3cabc
Binary files /dev/null and b/packages/app-store/make/static/4.jpeg differ
diff --git a/packages/app-store/make/static/5.jpeg b/packages/app-store/make/static/5.jpeg
new file mode 100644
index 0000000000..29667aff9a
Binary files /dev/null and b/packages/app-store/make/static/5.jpeg differ
diff --git a/packages/app-store/make/static/icon.svg b/packages/app-store/make/static/icon.svg
new file mode 100644
index 0000000000..3332f2c81f
--- /dev/null
+++ b/packages/app-store/make/static/icon.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/packages/app-store/make/zod.ts b/packages/app-store/make/zod.ts
new file mode 100644
index 0000000000..4a98d79116
--- /dev/null
+++ b/packages/app-store/make/zod.ts
@@ -0,0 +1,7 @@
+import { z } from "zod";
+
+export const appDataSchema = z.object({});
+
+export const appKeysSchema = z.object({
+ invite_link: z.string().min(1),
+});
diff --git a/packages/app-store/zapier/api/subscriptions/addSubscription.ts b/packages/app-store/zapier/api/subscriptions/addSubscription.ts
index 3bc3ecc2f4..675f161be1 100644
--- a/packages/app-store/zapier/api/subscriptions/addSubscription.ts
+++ b/packages/app-store/zapier/api/subscriptions/addSubscription.ts
@@ -1,12 +1,8 @@
-import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
-import { v4 } from "uuid";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
-import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
+import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
-import prisma from "@calcom/prisma";
-import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
@@ -23,45 +19,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const { subscriberUrl, triggerEvent } = req.body;
- try {
- const createSubscription = await prisma.webhook.create({
- data: {
- id: v4(),
- userId: validKey.userId,
- teamId: validKey.teamId,
- eventTriggers: [triggerEvent],
- subscriberUrl,
- active: true,
- appId: "zapier",
- },
- });
+ const createAppSubscription = await addSubscription({
+ appApiKey: validKey,
+ triggerEvent: triggerEvent,
+ subscriberUrl: subscriberUrl,
+ appId: "zapier",
+ });
- if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) {
- //schedule job for already existing bookings
- const where: Prisma.BookingWhereInput = {};
- if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
- else where.userId = validKey.userId;
- const bookings = await prisma.booking.findMany({
- where: {
- ...where,
- startTime: {
- gte: new Date(),
- },
- status: BookingStatus.ACCEPTED,
- },
- });
-
- for (const booking of bookings) {
- scheduleTrigger(booking, createSubscription.subscriberUrl, {
- id: createSubscription.id,
- appId: createSubscription.appId,
- });
- }
- }
- res.status(200).json(createSubscription);
- } catch (error) {
+ if (!createAppSubscription) {
return res.status(500).json({ message: "Could not create subscription." });
}
+
+ res.status(200).json(createAppSubscription);
}
export default defaultHandler({
diff --git a/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts b/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts
index b0887ac21b..81469a30ff 100644
--- a/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts
+++ b/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts
@@ -1,11 +1,9 @@
-import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
+import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
-import prisma from "@calcom/prisma";
-import { WebhookTriggerEvents } from "@calcom/prisma/enums";
const querySchema = z.object({
apiKey: z.string(),
@@ -24,49 +22,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
- const webhook = await prisma.webhook.findFirst({
- where: {
- id,
- userId: validKey.userId,
- teamId: validKey.teamId,
- },
+
+ const deleteEventSubscription = await deleteSubscription({
+ appApiKey: validKey,
+ webhookId: id,
+ appId: "zapier",
});
- if (!webhook) {
- return res.status(401).json({ message: "Not authorized to delete this webhook" });
+ if (!deleteEventSubscription) {
+ return res.status(500).json({ message: "Could not delete subscription." });
}
- if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) {
- const where: Prisma.BookingWhereInput = {};
- if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
- else where.userId = validKey.userId;
- const bookingsWithScheduledJobs = await prisma.booking.findMany({
- where: {
- ...where,
- scheduledJobs: {
- isEmpty: false,
- },
- },
- });
- for (const booking of bookingsWithScheduledJobs) {
- const updatedScheduledJobs = booking.scheduledJobs.filter(
- (scheduledJob) => scheduledJob !== `zapier_${webhook.id}`
- );
- await prisma.booking.update({
- where: {
- id: booking.id,
- },
- data: {
- scheduledJobs: updatedScheduledJobs,
- },
- });
- }
- }
-
- await prisma.webhook.delete({
- where: {
- id,
- },
- });
res.status(204).json({ message: "Subscription is deleted." });
}
diff --git a/packages/app-store/zapier/api/subscriptions/listBookings.ts b/packages/app-store/zapier/api/subscriptions/listBookings.ts
index 7cf0510339..f3a59c690a 100644
--- a/packages/app-store/zapier/api/subscriptions/listBookings.ts
+++ b/packages/app-store/zapier/api/subscriptions/listBookings.ts
@@ -1,11 +1,8 @@
-import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
-import { getHumanReadableLocationValue } from "@calcom/core/location";
-import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
-import { defaultHandler, defaultResponder, getTranslation } from "@calcom/lib/server";
-import prisma from "@calcom/prisma";
+import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger";
+import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
@@ -20,88 +17,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).json({ message: "API key not valid" });
}
- try {
- const where: Prisma.BookingWhereInput = {};
- if (validKey.teamId) {
- where.eventType = {
- OR: [{ teamId: validKey.teamId }, { parent: { teamId: validKey.teamId } }],
- };
- } else {
- where.userId = validKey.userId;
- }
+ const bookings = await listBookings(validKey);
- const bookings = await prisma.booking.findMany({
- take: 3,
- where,
- orderBy: {
- id: "desc",
- },
- select: {
- title: true,
- description: true,
- customInputs: true,
- responses: true,
- startTime: true,
- endTime: true,
- location: true,
- cancellationReason: true,
- status: true,
- user: {
- select: {
- username: true,
- name: true,
- email: true,
- timeZone: true,
- locale: true,
- },
- },
- eventType: {
- select: {
- title: true,
- description: true,
- requiresConfirmation: true,
- price: true,
- currency: true,
- length: true,
- bookingFields: true,
- team: true,
- },
- },
- attendees: {
- select: {
- name: true,
- email: true,
- timeZone: true,
- },
- },
- },
- });
-
- if (bookings.length === 0) {
- const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
- return res.status(404).json({
- message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
- });
- }
-
- const t = await getTranslation(bookings[0].user?.locale ?? "en", "common");
-
- const updatedBookings = bookings.map((booking) => {
- return {
- ...booking,
- ...getCalEventResponses({
- bookingFields: booking.eventType?.bookingFields ?? null,
- booking,
- }),
- location: getHumanReadableLocationValue(booking.location || "", t),
- };
- });
-
- res.status(201).json(updatedBookings);
- } catch (error) {
- console.error(error);
+ if (!bookings) {
return res.status(500).json({ message: "Unable to get bookings." });
}
+ if (bookings.length === 0) {
+ const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
+ return res.status(404).json({
+ message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
+ });
+ }
+ res.status(201).json(bookings);
}
export default defaultHandler({
diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts
index 8b610d55a7..aa5409101b 100644
--- a/packages/features/webhooks/lib/scheduleTrigger.ts
+++ b/packages/features/webhooks/lib/scheduleTrigger.ts
@@ -1,5 +1,205 @@
+import type { Prisma } from "@prisma/client";
+import { v4 } from "uuid";
+
+import { getHumanReadableLocationValue } from "@calcom/core/location";
+import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
+import logger from "@calcom/lib/logger";
+import { getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
-import { WebhookTriggerEvents } from "@calcom/prisma/enums";
+import type { ApiKey } from "@calcom/prisma/client";
+import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
+
+const log = logger.getChildLogger({ prefix: ["[node-scheduler]"] });
+
+export async function addSubscription({
+ appApiKey,
+ triggerEvent,
+ subscriberUrl,
+ appId,
+}: {
+ appApiKey: ApiKey;
+ triggerEvent: WebhookTriggerEvents;
+ subscriberUrl: string;
+ appId: string;
+}) {
+ try {
+ const createSubscription = await prisma.webhook.create({
+ data: {
+ id: v4(),
+ userId: appApiKey.userId,
+ teamId: appApiKey.teamId,
+ eventTriggers: [triggerEvent],
+ subscriberUrl,
+ active: true,
+ appId: appId,
+ },
+ });
+
+ if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) {
+ //schedule job for already existing bookings
+ const where: Prisma.BookingWhereInput = {};
+ if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId };
+ else where.userId = appApiKey.userId;
+ const bookings = await prisma.booking.findMany({
+ where: {
+ ...where,
+ startTime: {
+ gte: new Date(),
+ },
+ status: BookingStatus.ACCEPTED,
+ },
+ });
+
+ for (const booking of bookings) {
+ scheduleTrigger(booking, createSubscription.subscriberUrl, {
+ id: createSubscription.id,
+ appId: createSubscription.appId,
+ });
+ }
+ }
+
+ return createSubscription;
+ } catch (error) {
+ log.error(`Error creating subscription for user ${appApiKey.userId} and appId ${appApiKey.appId}.`);
+ }
+}
+
+export async function deleteSubscription({
+ appApiKey,
+ webhookId,
+ appId,
+}: {
+ appApiKey: ApiKey;
+ webhookId: string;
+ appId: string;
+}) {
+ try {
+ const webhook = await prisma.webhook.findFirst({
+ where: {
+ id: webhookId,
+ },
+ });
+
+ if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) {
+ const where: Prisma.BookingWhereInput = {};
+ if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId };
+ else where.userId = appApiKey.userId;
+ const bookingsWithScheduledJobs = await prisma.booking.findMany({
+ where: {
+ ...where,
+ scheduledJobs: {
+ isEmpty: false,
+ },
+ },
+ });
+ for (const booking of bookingsWithScheduledJobs) {
+ const updatedScheduledJobs = booking.scheduledJobs.filter(
+ (scheduledJob) => scheduledJob !== `${appId}_${webhook.id}`
+ );
+ await prisma.booking.update({
+ where: {
+ id: booking.id,
+ },
+ data: {
+ scheduledJobs: updatedScheduledJobs,
+ },
+ });
+ }
+ }
+
+ const deleteWebhook = await prisma.webhook.delete({
+ where: {
+ id: webhookId,
+ },
+ });
+ if (!deleteWebhook) {
+ throw new Error(`Unable to delete webhook ${webhookId}`);
+ }
+ return deleteWebhook;
+ } catch (err) {
+ log.error(
+ `Error deleting subscription for user ${appApiKey.userId}, webhookId ${webhookId}, appId ${appId}`
+ );
+ }
+}
+
+export async function listBookings(appApiKey: ApiKey) {
+ try {
+ const where: Prisma.BookingWhereInput = {};
+ if (appApiKey.teamId) {
+ where.eventType = {
+ OR: [{ teamId: appApiKey.teamId }, { parent: { teamId: appApiKey.teamId } }],
+ };
+ } else {
+ where.userId = appApiKey.userId;
+ }
+ const bookings = await prisma.booking.findMany({
+ take: 3,
+ where: where,
+ orderBy: {
+ id: "desc",
+ },
+ select: {
+ title: true,
+ description: true,
+ customInputs: true,
+ responses: true,
+ startTime: true,
+ endTime: true,
+ location: true,
+ cancellationReason: true,
+ status: true,
+ user: {
+ select: {
+ username: true,
+ name: true,
+ email: true,
+ timeZone: true,
+ locale: true,
+ },
+ },
+ eventType: {
+ select: {
+ title: true,
+ description: true,
+ requiresConfirmation: true,
+ price: true,
+ currency: true,
+ length: true,
+ bookingFields: true,
+ team: true,
+ },
+ },
+ attendees: {
+ select: {
+ name: true,
+ email: true,
+ timeZone: true,
+ },
+ },
+ },
+ });
+ if (bookings.length === 0) {
+ return [];
+ }
+ const t = await getTranslation(bookings[0].user?.locale ?? "en", "common");
+
+ const updatedBookings = bookings.map((booking) => {
+ return {
+ ...booking,
+ ...getCalEventResponses({
+ bookingFields: booking.eventType?.bookingFields ?? null,
+ booking,
+ }),
+ location: getHumanReadableLocationValue(booking.location || "", t),
+ };
+ });
+
+ return updatedBookings;
+ } catch (err) {
+ log.error(`Error retrieving list of bookings for user ${appApiKey.userId} and appId ${appApiKey.appId}.`);
+ }
+}
export async function scheduleTrigger(
booking: { id: number; endTime: Date; scheduledJobs: string[] },
diff --git a/packages/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts
index 56dd6e1133..d4bff45b70 100644
--- a/packages/prisma/seed-app-store.ts
+++ b/packages/prisma/seed-app-store.ts
@@ -337,7 +337,9 @@ export default async function main() {
invite_link: process.env.ZAPIER_INVITE_LINK,
});
}
-
+ await createApp("make", "make", ["automation"], "make_automation", {
+ invite_link: "https://make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510",
+ });
await createApp("huddle01", "huddle01video", ["conferencing"], "huddle01_video");
// Payment apps