From 1010e2894a278274602652df72f5d4271b914f86 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Thu, 17 Nov 2022 18:38:34 -0300 Subject: [PATCH] Bringing back sendgrid app to review (#5501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sendgrid app and code simplification * Applying app-store-cli + impl * Fixing types * Adding features to readme * Fixing unit tests * A few last tweaks regarding UX and env vars * Applying feedback * Using calcom icons * Renaming and applying feedback * Testing user/type page fix * Standarizing Sendgrid client usage * Removing types * Reverting CloseCom changes * Stop relying on sendgrid client pkg * Fixing button and more reverting closecom changes * Revert "Stop relying on sendgrid client pkg" This reverts commit dd61851572a17a1e4051b133683af85c934bc2d0. * Revert "Removing types" This reverts commit 1ec5ed8de2f3139bbe84f867f229bc5759256806. * Is this it? * Standardizing apis * Fixing path * Fixing throwing errors the standard way * Stop relying on getInstalledAppPath * Removing seemingly troubling code * Returning error and avoiding any outer reference * Revert "Returning error and avoiding any outer reference" This reverts commit 7d32e30154423c95f54ebae81a76ab16a1c7bc94. * Revert "Removing seemingly troubling code" This reverts commit eaae772abcd04c8f046e4960116f42c5aaf87adf. * Revert "Stop relying on getInstalledAppPath" This reverts commit bcc70fc337bbe7fb5e74609abaeee7cd3ede90a3. * Revert "Fixing throwing errors the standard way" This reverts commit bb1bb410fac6f8c6ad14c3163a8433d125f7a885. * Revert "Fixing path" This reverts commit a7bd83c4fb7597594d0470cb530378c826b45481. * Revert "Standardizing apis" This reverts commit 0258a182298af3ebad321854ef4f34a65f4c700a. * Revert "Is this it?" This reverts commit 70b3f7b98e3003dfa225dc539e02a1e17abdd840. * Converting APIs to legacy style * Missing reverted CloseCom test mock * Needed for the renaming * Reverting Closecom and yarn unneeded changes * Ununsed type * Testing rearranging exports * Update apps/web/components/apps/OmniInstallAppButton.tsx Co-authored-by: Omar López * Standardizing APIs * Fixing wrong toast message on app page Co-authored-by: Peer Richelsen Co-authored-by: Omar López --- .env.example | 6 +- app.json | 4 + apps/web/components/apps/App.tsx | 3 +- .../components/apps/IntegrationListItem.tsx | 8 + .../components/apps/OmniInstallAppButton.tsx | 3 +- apps/web/pages/apps/installed/[category].tsx | 4 +- packages/app-store/_pages/setup/index.tsx | 1 + packages/app-store/_utils/getCalendar.ts | 5 +- .../app-store/_utils/useAddAppMutation.ts | 23 ++- packages/app-store/apps.metadata.generated.ts | 2 + packages/app-store/apps.server.generated.ts | 1 + .../closecomothercalendar/test/globals.ts | 11 +- .../test/lib/CalendarService.test.ts | 2 +- .../hubspotothercalendar/api/callback.ts | 2 +- packages/app-store/index.ts | 2 + packages/app-store/sendgrid/README.mdx | 10 ++ packages/app-store/sendgrid/_metadata.ts | 10 ++ packages/app-store/sendgrid/api/_getAdd.ts | 14 ++ packages/app-store/sendgrid/api/_postAdd.ts | 39 +++++ packages/app-store/sendgrid/api/add.ts | 6 + packages/app-store/sendgrid/api/check.ts | 31 ++++ packages/app-store/sendgrid/api/index.ts | 2 + packages/app-store/sendgrid/config.json | 16 ++ packages/app-store/sendgrid/index.ts | 3 + .../app-store/sendgrid/lib/CalendarService.ts | 99 ++++++++++++ packages/app-store/sendgrid/lib/index.ts | 1 + packages/app-store/sendgrid/package.json | 16 ++ .../app-store/sendgrid/pages/setup/index.tsx | 152 ++++++++++++++++++ packages/app-store/sendgrid/static/1.png | Bin 0 -> 20222 bytes packages/app-store/sendgrid/static/logo.png | Bin 0 -> 4472 bytes .../app-store/zapier/pages/setup/index.tsx | 2 +- packages/lib/Sendgrid.ts | 129 +++++++++++++++ packages/lib/server/defaultHandler.ts | 4 +- packages/lib/server/defaultResponder.ts | 4 +- packages/lib/server/index.ts | 4 +- packages/lib/sync/services/CloseComService.ts | 4 +- packages/lib/sync/services/SendgridService.ts | 122 ++------------ packages/prisma/seed-app-store.config.json | 6 + packages/ui/v2/core/apps/AppCard.tsx | 3 +- turbo.json | 1 + 40 files changed, 613 insertions(+), 142 deletions(-) create mode 100644 packages/app-store/sendgrid/README.mdx create mode 100644 packages/app-store/sendgrid/_metadata.ts create mode 100644 packages/app-store/sendgrid/api/_getAdd.ts create mode 100644 packages/app-store/sendgrid/api/_postAdd.ts create mode 100644 packages/app-store/sendgrid/api/add.ts create mode 100644 packages/app-store/sendgrid/api/check.ts create mode 100644 packages/app-store/sendgrid/api/index.ts create mode 100644 packages/app-store/sendgrid/config.json create mode 100644 packages/app-store/sendgrid/index.ts create mode 100644 packages/app-store/sendgrid/lib/CalendarService.ts create mode 100644 packages/app-store/sendgrid/lib/index.ts create mode 100644 packages/app-store/sendgrid/package.json create mode 100644 packages/app-store/sendgrid/pages/setup/index.tsx create mode 100644 packages/app-store/sendgrid/static/1.png create mode 100644 packages/app-store/sendgrid/static/logo.png create mode 100644 packages/lib/Sendgrid.ts diff --git a/.env.example b/.env.example index 295447add0..221c962a6d 100644 --- a/.env.example +++ b/.env.example @@ -77,7 +77,7 @@ NEXT_PUBLIC_HELPSCOUT_KEY= SEND_FEEDBACK_EMAIL= # Sengrid -# Used for email reminders in workflows +# Used for email reminders in workflows and internal sync services SENDGRID_API_KEY= SENDGRID_EMAIL= @@ -134,8 +134,8 @@ NEXT_PUBLIC_TEAM_IMPERSONATION=false # Close.com internal CRM CLOSECOM_API_KEY= -# Sendgrid internal email sender -SENDGRID_API_KEY= +# Sendgrid internal sync service +SENDGRID_SYNC_API_KEY= # Sentry NEXT_PUBLIC_SENTRY_DSN= diff --git a/app.json b/app.json index 2ad52b916c..ba670bab19 100644 --- a/app.json +++ b/app.json @@ -50,6 +50,10 @@ "description": "Sendgrid api key. Used for email reminders in workflows", "value": "" }, + "SENDGRID_SYNC_API_KEY": { + "description": "Sendgrid internal sync service", + "value": "" + }, "SENDGRID_EMAIL": { "description": "Sendgrid email. Used for email reminders in workflows", "value": "" diff --git a/apps/web/components/apps/App.tsx b/apps/web/components/apps/App.tsx index 4dd6a739f9..f04d2083dc 100644 --- a/apps/web/components/apps/App.tsx +++ b/apps/web/components/apps/App.tsx @@ -43,7 +43,8 @@ const Component = ({ const router = useRouter(); const mutation = useAddAppMutation(null, { - onSuccess: () => { + onSuccess: (data) => { + if (data.setupPending) return; showToast(t("app_successfully_installed"), "success"); }, onError: (error) => { diff --git a/apps/web/components/apps/IntegrationListItem.tsx b/apps/web/components/apps/IntegrationListItem.tsx index 03198de203..a27d424371 100644 --- a/apps/web/components/apps/IntegrationListItem.tsx +++ b/apps/web/components/apps/IntegrationListItem.tsx @@ -4,6 +4,7 @@ import { ReactNode, useEffect, useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Icon } from "@calcom/ui"; +import { showToast } from "@calcom/ui/v2"; import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List"; import classNames from "@lib/classNames"; @@ -27,6 +28,13 @@ function IntegrationListItem(props: { const [highlight, setHighlight] = useState(hl === props.slug); const title = props.name || props.title; + // The highlight is to show a newly installed app, coming from the app's + // redirection after installation, so we proceed to show the corresponding + // message + if (highlight) { + showToast(t("app_successfully_installed"), "success"); + } + useEffect(() => { const timer = setTimeout(() => setHighlight(false), 3000); return () => { diff --git a/apps/web/components/apps/OmniInstallAppButton.tsx b/apps/web/components/apps/OmniInstallAppButton.tsx index b76e09bbb0..0883016823 100644 --- a/apps/web/components/apps/OmniInstallAppButton.tsx +++ b/apps/web/components/apps/OmniInstallAppButton.tsx @@ -18,10 +18,11 @@ export default function OmniInstallAppButton({ appId, className }: { appId: stri const utils = trpc.useContext(); const mutation = useAddAppMutation(null, { - onSuccess: () => { + onSuccess: (data) => { //TODO: viewer.appById might be replaced with viewer.apps so that a single query needs to be invalidated. utils.viewer.appById.invalidate({ appId }); utils.viewer.apps.invalidate({ extendsFeature: "EventType" }); + if (data.setupPending) return; showToast(t("app_successfully_installed"), "success"); }, onError: (error) => { diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index 7a091f795b..da92ec8117 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -179,7 +179,9 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps) })} description={t(`no_category_apps_description_${variant || "other"}`)} buttonRaw={ - } diff --git a/packages/app-store/_pages/setup/index.tsx b/packages/app-store/_pages/setup/index.tsx index bb08cc9e6c..9259f750ad 100644 --- a/packages/app-store/_pages/setup/index.tsx +++ b/packages/app-store/_pages/setup/index.tsx @@ -10,6 +10,7 @@ export const AppSetupMap = { "caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")), zapier: dynamic(() => import("../../zapier/pages/setup")), closecom: dynamic(() => import("../../closecomothercalendar/pages/setup")), + sendgrid: dynamic(() => import("../../sendgrid/pages/setup")), }; export const AppSetupPage = (props: { slug: string }) => { diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 742ac51a7a..213f150ae0 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -8,7 +8,10 @@ const log = logger.getChildLogger({ prefix: ["CalendarManager"] }); export const getCalendar = (credential: CredentialPayload | null): Calendar | null => { if (!credential || !credential.key) return null; - const { type: calendarType } = credential; + let { type: calendarType } = credential; + if (calendarType === "sendgrid_other_calendar") { + calendarType = "sendgrid"; + } const calendarApp = appStore[calendarType.split("_").join("") as keyof typeof appStore]; if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) { log.warn(`calendar of type ${calendarType} is not implemented`); diff --git a/packages/app-store/_utils/useAddAppMutation.ts b/packages/app-store/_utils/useAddAppMutation.ts index 5cdf68d60e..e85de87d76 100644 --- a/packages/app-store/_utils/useAddAppMutation.ts +++ b/packages/app-store/_utils/useAddAppMutation.ts @@ -1,4 +1,4 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import type { IntegrationOAuthCallbackState } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -14,9 +14,17 @@ function gotoUrl(url: string, newTab?: boolean) { window.location.href = url; } -function useAddAppMutation(_type: App["type"] | null, options?: Parameters[2]) { +type CustomUseMutationOptions = + | Omit, "mutationKey" | "mutationFn" | "onSuccess"> + | undefined; + +type UseAddAppMutationOptions = CustomUseMutationOptions & { + onSuccess: (data: { setupPending: boolean }) => void; +}; + +function useAddAppMutation(_type: App["type"] | null, options?: UseAddAppMutationOptions) { const mutation = useMutation< - unknown, + { setupPending: boolean }, Error, { type?: App["type"]; variant?: string; slug?: string; isOmniInstall?: boolean } | "" >(async (variables) => { @@ -28,6 +36,9 @@ function useAddAppMutation(_type: App["type"] | null, options?: Parameters ({ - debug: jest.fn(), - error: jest.fn(), - log: jest.fn(), - getChildLogger: jest.fn(), + default: { + getChildLogger: () => ({ + debug: jest.fn(), + error: jest.fn(), + log: jest.fn(), + }), + }, })); jest.mock("@calcom/lib/crypto", () => ({ diff --git a/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts b/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts index da4e316a6d..ee9d6b2284 100644 --- a/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts +++ b/packages/app-store/closecomothercalendar/test/lib/CalendarService.test.ts @@ -168,7 +168,7 @@ test("prepare data to create custom activity type instance: two attendees, no ad const event = { attendees, startTime: now.toISOString(), - } as CalendarEvent; + } as unknown as CalendarEvent; CloseCom.prototype.activity = { type: { diff --git a/packages/app-store/hubspotothercalendar/api/callback.ts b/packages/app-store/hubspotothercalendar/api/callback.ts index 66de827e24..0ce3498522 100644 --- a/packages/app-store/hubspotothercalendar/api/callback.ts +++ b/packages/app-store/hubspotothercalendar/api/callback.ts @@ -57,6 +57,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const state = decodeOAuthState(req); res.redirect( - getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "other_calendar", slug: "hubspot" }) + getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "other", slug: "hubspot" }) ); } diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 6c87e4b0aa..4082532457 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -15,6 +15,7 @@ import * as jitsivideo from "./jitsivideo"; import * as larkcalendar from "./larkcalendar"; import * as office365calendar from "./office365calendar"; import * as office365video from "./office365video"; +import * as sendgrid from "./sendgrid"; import * as stripepayment from "./stripepayment"; import * as tandemvideo from "./tandemvideo"; import * as vital from "./vital"; @@ -36,6 +37,7 @@ const appStore = { larkcalendar, office365calendar, office365video, + sendgrid, stripepayment, tandemvideo, vital, diff --git a/packages/app-store/sendgrid/README.mdx b/packages/app-store/sendgrid/README.mdx new file mode 100644 index 0000000000..c716fa4ba4 --- /dev/null +++ b/packages/app-store/sendgrid/README.mdx @@ -0,0 +1,10 @@ +--- +description: SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform. +items: + - /api/app-store/sendgrid/1.png +--- + +{description} + +Features: + - Creates event attendees as contacts in Sendgrid diff --git a/packages/app-store/sendgrid/_metadata.ts b/packages/app-store/sendgrid/_metadata.ts new file mode 100644 index 0000000000..9c7f2aa320 --- /dev/null +++ b/packages/app-store/sendgrid/_metadata.ts @@ -0,0 +1,10 @@ +import type { AppMeta } from "@calcom/types/App"; + +import config from "./config.json"; + +export const metadata = { + category: "other", + ...config, +} as AppMeta; + +export default metadata; diff --git a/packages/app-store/sendgrid/api/_getAdd.ts b/packages/app-store/sendgrid/api/_getAdd.ts new file mode 100644 index 0000000000..2b7ae8c535 --- /dev/null +++ b/packages/app-store/sendgrid/api/_getAdd.ts @@ -0,0 +1,14 @@ +import type { NextApiRequest } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; + +import checkSession from "../../_utils/auth"; +import { checkInstalled } from "../../_utils/installation"; + +export async function getHandler(req: NextApiRequest) { + const session = checkSession(req); + await checkInstalled("sendgrid", session.user?.id); + return { url: "/apps/sendgrid/setup" }; +} + +export default defaultResponder(getHandler); diff --git a/packages/app-store/sendgrid/api/_postAdd.ts b/packages/app-store/sendgrid/api/_postAdd.ts new file mode 100644 index 0000000000..1cf672fb95 --- /dev/null +++ b/packages/app-store/sendgrid/api/_postAdd.ts @@ -0,0 +1,39 @@ +import type { NextApiRequest } from "next"; + +import { symmetricEncrypt } from "@calcom/lib/crypto"; +import { HttpError } from "@calcom/lib/http-error"; +import logger from "@calcom/lib/logger"; +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import checkSession from "../../_utils/auth"; +import getInstalledAppPath from "../../_utils/getInstalledAppPath"; + +export async function getHandler(req: NextApiRequest) { + const session = checkSession(req); + + const { api_key } = req.body; + if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provided to check" }); + + const encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || ""); + + const data = { + type: "sendgrid_other_calendar", + key: { encrypted }, + userId: session.user?.id, + appId: "sendgrid", + }; + + try { + await prisma.credential.create({ + data, + }); + } catch (reason) { + logger.error("Could not add Sendgrid app", reason); + throw new HttpError({ statusCode: 500, message: "Could not add Sendgrid app" }); + } + + return { url: getInstalledAppPath({ variant: "other", slug: "sendgrid" }) }; +} + +export default defaultResponder(getHandler); diff --git a/packages/app-store/sendgrid/api/add.ts b/packages/app-store/sendgrid/api/add.ts new file mode 100644 index 0000000000..9480fb9259 --- /dev/null +++ b/packages/app-store/sendgrid/api/add.ts @@ -0,0 +1,6 @@ +import { defaultHandler } from "@calcom/lib/server"; + +export default defaultHandler({ + GET: import("./_getAdd"), + POST: import("./_postAdd"), +}); diff --git a/packages/app-store/sendgrid/api/check.ts b/packages/app-store/sendgrid/api/check.ts new file mode 100644 index 0000000000..98c695dd40 --- /dev/null +++ b/packages/app-store/sendgrid/api/check.ts @@ -0,0 +1,31 @@ +import type { NextApiRequest } from "next"; + +import Sendgrid from "@calcom/lib/Sendgrid"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +import checkSession from "../../_utils/auth"; + +export async function getHandler(req: NextApiRequest) { + const { api_key } = req.body; + if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provoided to check" }); + + checkSession(req); + + const sendgrid: Sendgrid = new Sendgrid(api_key); + + try { + const usernameInfo = await sendgrid.username(); + if (usernameInfo.username) { + return {}; + } else { + throw new HttpError({ statusCode: 404 }); + } + } catch (e) { + throw new HttpError({ statusCode: 500, message: e as string }); + } +} + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(getHandler) }), +}); diff --git a/packages/app-store/sendgrid/api/index.ts b/packages/app-store/sendgrid/api/index.ts new file mode 100644 index 0000000000..766afa6405 --- /dev/null +++ b/packages/app-store/sendgrid/api/index.ts @@ -0,0 +1,2 @@ +export { default as add } from "./add"; +export { default as check } from "./check"; diff --git a/packages/app-store/sendgrid/config.json b/packages/app-store/sendgrid/config.json new file mode 100644 index 0000000000..d95d4b11ce --- /dev/null +++ b/packages/app-store/sendgrid/config.json @@ -0,0 +1,16 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Sendgrid", + "slug": "sendgrid", + "type": "sendgrid_other_calendar", + "imageSrc": "/api/app-store/sendgrid/logo.png", + "logo": "/api/app-store/sendgrid/logo.png", + "url": "https://cal.com/apps/sendgrid", + "variant": "other_calendar", + "categories": ["other"], + "publisher": "Cal.com", + "email": "help@cal.com", + "description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.", + "extendsFeature": "User", + "__createdUsingCli": true +} diff --git a/packages/app-store/sendgrid/index.ts b/packages/app-store/sendgrid/index.ts new file mode 100644 index 0000000000..5373eb04ef --- /dev/null +++ b/packages/app-store/sendgrid/index.ts @@ -0,0 +1,3 @@ +export * as api from "./api"; +export * as lib from "./lib"; +export { metadata } from "./_metadata"; diff --git a/packages/app-store/sendgrid/lib/CalendarService.ts b/packages/app-store/sendgrid/lib/CalendarService.ts new file mode 100644 index 0000000000..17e69a623b --- /dev/null +++ b/packages/app-store/sendgrid/lib/CalendarService.ts @@ -0,0 +1,99 @@ +import z from "zod"; + +import Sendgrid, { SendgridNewContact } from "@calcom/lib/Sendgrid"; +import { symmetricDecrypt } from "@calcom/lib/crypto"; +import logger from "@calcom/lib/logger"; +import type { + Calendar, + CalendarEvent, + EventBusyDate, + IntegrationCalendar, + NewCalendarEventType, +} from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; + +const apiKeySchema = z.object({ + encrypted: z.string(), +}); + +const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || ""; + +/** + * Authentication + * Sendgrid requires Basic Auth for any request to their APIs, which is far from + * ideal considering that such a strategy requires generating an API Key by the + * user and input it in our system. A Setup page was created when trying to install + * Sendgrid in order to instruct how to create such resource and to obtain it. + */ +export default class CloseComCalendarService implements Calendar { + private integrationName = ""; + private sendgrid: Sendgrid; + private log: typeof logger; + + constructor(credential: CredentialPayload) { + this.integrationName = "sendgrid_other_calendar"; + this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); + + const parsedCredentialKey = apiKeySchema.safeParse(credential.key); + + let decrypted; + if (parsedCredentialKey.success) { + decrypted = symmetricDecrypt(parsedCredentialKey.data.encrypted, CALENDSO_ENCRYPTION_KEY); + const { api_key } = JSON.parse(decrypted); + this.sendgrid = new Sendgrid(api_key); + } else { + throw Error( + `No API Key found for userId ${credential.userId} and appId ${credential.appId}: ${parsedCredentialKey.error}` + ); + } + } + + async createEvent(event: CalendarEvent): Promise { + // Proceeding to just creating the user in Sendgrid, no event entity exists in Sendgrid + const contactsData = event.attendees.map((attendee) => ({ + first_name: attendee.name, + email: attendee.email, + })); + const result = await this.sendgrid.sendgridRequest({ + url: `/v3/marketing/contacts`, + method: "PUT", + body: { + contacts: contactsData, + }, + }); + return Promise.resolve({ + id: "", + uid: result.job_id, + password: "", + url: "", + type: this.integrationName, + additionalInfo: { + result, + }, + }); + } + + async updateEvent(uid: string, event: CalendarEvent): Promise { + // Unless we want to be able to support modifying an event to add more attendees + // to have them created in Sendgrid, ingoring this use case for now + return Promise.resolve(); + } + + async deleteEvent(uid: string): Promise { + // Unless we want to delete the contact in Sendgrid once the event + // is deleted just ingoring this use case for now + return Promise.resolve(); + } + + async getAvailability( + dateFrom: string, + dateTo: string, + selectedCalendars: IntegrationCalendar[] + ): Promise { + return Promise.resolve([]); + } + + async listCalendars(event?: CalendarEvent): Promise { + return Promise.resolve([]); + } +} diff --git a/packages/app-store/sendgrid/lib/index.ts b/packages/app-store/sendgrid/lib/index.ts new file mode 100644 index 0000000000..e168c149df --- /dev/null +++ b/packages/app-store/sendgrid/lib/index.ts @@ -0,0 +1 @@ +export { default as CalendarService } from "./CalendarService"; diff --git a/packages/app-store/sendgrid/package.json b/packages/app-store/sendgrid/package.json new file mode 100644 index 0000000000..0292fb930d --- /dev/null +++ b/packages/app-store/sendgrid/package.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/sendgrid", + "version": "0.0.0", + "main": "./index.ts", + "description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.", + "dependencies": { + "@calcom/lib": "*", + "@calcom/prisma": "*", + "@sendgrid/client": "^7.7.0" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/sendgrid/pages/setup/index.tsx b/packages/app-store/sendgrid/pages/setup/index.tsx new file mode 100644 index 0000000000..9fecd4df81 --- /dev/null +++ b/packages/app-store/sendgrid/pages/setup/index.tsx @@ -0,0 +1,152 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/router"; +import { useState, useEffect } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { Toaster } from "react-hot-toast"; +import z from "zod"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Icon } from "@calcom/ui/Icon"; +import { Button } from "@calcom/ui/components/button"; +import { Form, TextField } from "@calcom/ui/components/form"; +import { showToast } from "@calcom/ui/v2"; + +const formSchema = z.object({ + api_key: z.string(), +}); + +export default function SendgridSetup() { + const { t } = useLocale(); + const router = useRouter(); + const [testPassed, setTestPassed] = useState(undefined); + const [testLoading, setTestLoading] = useState(false); + + const form = useForm<{ + api_key: string; + }>({ + resolver: zodResolver(formSchema), + }); + + useEffect(() => { + const timer = setTimeout(() => { + if (testPassed === false) { + setTestPassed(undefined); + } + }, 3000); + return () => clearTimeout(timer); + }, [testPassed]); + + return ( +
+
+
+
+ Sendgrid +
+
+

{t("provide_api_key")}

+ +
+ {t("generate_api_key_description")}{" "} + + Sendgrid + + . {t("it_stored_encrypted")} +
+
+
{ + const res = await fetch("/api/integrations/sendgrid/add", { + method: "POST", + body: JSON.stringify(values), + headers: { + "Content-Type": "application/json", + }, + }); + const json = await res.json(); + + if (res.ok) { + router.push(json.url); + } else { + showToast(json.message, "error"); + } + }}> +
+ ( + { + onChange(e.target.value); + form.setValue("api_key", e.target.value); + await form.trigger("api_key"); + }} + /> + )} + /> +
+
+ + + +
+
+
+
+
+
+ +
+ ); +} diff --git a/packages/app-store/sendgrid/static/1.png b/packages/app-store/sendgrid/static/1.png new file mode 100644 index 0000000000000000000000000000000000000000..ead1889ab2681ebb983bb0897dc6c7581791d443 GIT binary patch literal 20222 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 JU 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 literal 0 HcmV?d00001 diff --git a/packages/app-store/zapier/pages/setup/index.tsx b/packages/app-store/zapier/pages/setup/index.tsx index ce79c651ec..5501bfcb95 100644 --- a/packages/app-store/zapier/pages/setup/index.tsx +++ b/packages/app-store/zapier/pages/setup/index.tsx @@ -105,7 +105,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
  • You're set!
  • - + diff --git a/packages/lib/Sendgrid.ts b/packages/lib/Sendgrid.ts new file mode 100644 index 0000000000..bf9fe90809 --- /dev/null +++ b/packages/lib/Sendgrid.ts @@ -0,0 +1,129 @@ +import client from "@sendgrid/client"; +import { ClientRequest } from "@sendgrid/client/src/request"; +import { ClientResponse } from "@sendgrid/client/src/response"; + +import logger from "@calcom/lib/logger"; + +export type SendgridFieldOptions = [string, string][]; + +type SendgridUsernameResult = { + username: string; + user_id: number; +}; + +export type SendgridCustomField = { + id: string; + name: string; + field_type: string; + _metadata: { + self: string; + }; +}; + +export type SendgridContact = { + id: string; + first_name: string; + last_name: string; + email: string; +}; + +export type SendgridSearchResult = { + result: SendgridContact[]; +}; + +export type SendgridFieldDefinitions = { + custom_fields: SendgridCustomField[]; +}; + +export type SendgridNewContact = { + job_id: string; +}; + +const environmentApiKey = process.env.SENDGRID_SYNC_API_KEY || ""; + +/** + * This class to instance communicating to Sendgrid APIs requires an API Key. + * + * You can either pass to the constructor an API Key or have one defined as an + * environment variable in case the communication to Sendgrid is just for + * one account only, not configurable by any user at any moment. + */ +export default class Sendgrid { + private log: typeof logger; + + constructor(providedApiKey = "") { + this.log = logger.getChildLogger({ prefix: [`[[lib] sendgrid`] }); + if (!providedApiKey && !environmentApiKey) throw Error("Sendgrid Api Key not present"); + client.setApiKey(providedApiKey || environmentApiKey); + } + + public username = async () => { + const username = await this.sendgridRequest({ + url: `/v3/user/username`, + method: "GET", + }); + return username; + }; + + public async sendgridRequest(data: ClientRequest): Promise { + this.log.debug("sendgridRequest:request", data); + const results = await client.request(data); + this.log.debug("sendgridRequest:results", results); + if (results[1].errors) throw Error(`Sendgrid request error: ${results[1].errors}`); + return results[1]; + } + + public async getSendgridContactId(email: string) { + const search = await this.sendgridRequest({ + url: `/v3/marketing/contacts/search`, + method: "POST", + body: { + query: `email LIKE '${email}'`, + }, + }); + this.log.debug("sync:sendgrid:getSendgridContactId:search", search); + return search.result || []; + } + + public async getSendgridCustomFieldsIds(customFields: SendgridFieldOptions) { + // Get Custom Activity Fields + const allFields = await this.sendgridRequest({ + url: `/v3/marketing/field_definitions`, + method: "GET", + }); + allFields.custom_fields = allFields.custom_fields ?? []; + this.log.debug("sync:sendgrid:getCustomFieldsIds:allFields", allFields); + const customFieldsNames = allFields.custom_fields.map((fie) => fie.name); + this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsNames", customFieldsNames); + const customFieldsExist = customFields.map((cusFie) => customFieldsNames.includes(cusFie[0])); + this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsExist", customFieldsExist); + return await Promise.all( + customFieldsExist.map(async (exist, idx) => { + if (!exist) { + const [name, field_type] = customFields[idx]; + const created = await this.sendgridRequest({ + url: `/v3/marketing/field_definitions`, + method: "POST", + body: { + name, + field_type, + }, + }); + this.log.debug("sync:sendgrid:getCustomFieldsIds:customField:created", created); + return created.id; + } else { + const index = customFieldsNames.findIndex((val) => val === customFields[idx][0]); + if (index >= 0) { + this.log.debug( + "sync:sendgrid:getCustomFieldsIds:customField:existed", + allFields.custom_fields[index].id + ); + return allFields.custom_fields[index].id; + } else { + throw Error("Couldn't find the field index"); + } + } + }) + ); + } +} diff --git a/packages/lib/server/defaultHandler.ts b/packages/lib/server/defaultHandler.ts index ded7ad9956..1cfa351bd7 100644 --- a/packages/lib/server/defaultHandler.ts +++ b/packages/lib/server/defaultHandler.ts @@ -5,7 +5,7 @@ type Handlers = { }; /** Allows us to split big API handlers by method */ -const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => { +export const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => { const handler = (await handlers[req.method as keyof typeof handlers])?.default; // auto catch unsupported methods. if (!handler) { @@ -22,5 +22,3 @@ const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: return res.status(500).json({ message: "Something went wrong" }); } }; - -export default defaultHandler; diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts index 4a24bc350b..7caf1e1b51 100644 --- a/packages/lib/server/defaultResponder.ts +++ b/packages/lib/server/defaultResponder.ts @@ -6,7 +6,7 @@ import { performance } from "./perfObserver"; type Handle = (req: NextApiRequest, res: NextApiResponse) => Promise; /** Allows us to get type inference from API handler responses */ -function defaultResponder(f: Handle) { +export function defaultResponder(f: Handle) { return async (req: NextApiRequest, res: NextApiResponse) => { let ok = false; try { @@ -25,5 +25,3 @@ function defaultResponder(f: Handle) { } }; } - -export default defaultResponder; diff --git a/packages/lib/server/index.ts b/packages/lib/server/index.ts index 64f13128d3..53253fe2c4 100644 --- a/packages/lib/server/index.ts +++ b/packages/lib/server/index.ts @@ -1,7 +1,7 @@ export { checkBookingLimits, checkLimit } from "./checkBookingLimits"; -export { default as defaultHandler } from "./defaultHandler"; -export { default as defaultResponder } from "./defaultResponder"; +export { defaultHandler } from "./defaultHandler"; +export { defaultResponder } from "./defaultResponder"; export { getLuckyUser } from "./getLuckyUser"; export { getServerErrorFromUnknown } from "./getServerErrorFromUnknown"; export { getTranslation } from "./i18n"; diff --git a/packages/lib/sync/services/CloseComService.ts b/packages/lib/sync/services/CloseComService.ts index bece375096..8acbd3e6c5 100644 --- a/packages/lib/sync/services/CloseComService.ts +++ b/packages/lib/sync/services/CloseComService.ts @@ -20,6 +20,8 @@ const calComSharedFields: CloseComFieldOptions = [["Contact Role", "text", false const serviceName = "closecom_service"; export default class CloseComService extends SyncServiceCore implements ISyncService { + protected declare service: CloseCom; + constructor() { super(serviceName, CloseCom, logger.getChildLogger({ prefix: [`[[sync] ${serviceName}`] })); } @@ -120,7 +122,7 @@ export default class CloseComService extends SyncServiceCore implements ISyncSer this.log.debug("sync:closecom:web:team:update", { prevTeam, updatedTeam }); const leadId = await getCloseComLeadId(this.service, { companyName: prevTeam.name }); this.log.debug("sync:closecom:web:team:update:leadId", { leadId }); - this.service.lead.update(leadId, updatedTeam); + this.service.lead.update(leadId, { companyName: updatedTeam.name }); }, }, membership: { diff --git a/packages/lib/sync/services/SendgridService.ts b/packages/lib/sync/services/SendgridService.ts index d2e679ede9..cfc26e460a 100644 --- a/packages/lib/sync/services/SendgridService.ts +++ b/packages/lib/sync/services/SendgridService.ts @@ -1,41 +1,11 @@ -import sendgrid from "@sendgrid/client"; -import { ClientRequest } from "@sendgrid/client/src/request"; -import { ClientResponse } from "@sendgrid/client/src/response"; - import logger from "@calcom/lib/logger"; -import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "@calcom/lib/sync/ISyncService"; -import SyncServiceCore from "@calcom/lib/sync/ISyncService"; -type SendgridCustomField = { - id: string; - name: string; - field_type: string; - _metadata: { - self: string; - }; -}; - -type SendgridContact = { - id: string; - first_name: string; - last_name: string; - email: string; -}; - -type SendgridSearchResult = { - result: SendgridContact[]; -}; - -type SendgridFieldDefinitions = { - custom_fields: SendgridCustomField[]; -}; - -type SendgridNewContact = { - job_id: string; -}; +import Sendgrid, { SendgridFieldOptions, SendgridNewContact } from "../../Sendgrid"; +import ISyncService, { ConsoleUserInfoType, WebUserInfoType } from "../ISyncService"; +import SyncServiceCore from "../ISyncService"; // Cal.com Custom Contact Fields -const calComCustomContactFields: [string, string][] = [ +const calComCustomContactFields: SendgridFieldOptions = [ // Field name, field type ["username", "Text"], ["plan", "Text"], @@ -43,92 +13,18 @@ const calComCustomContactFields: [string, string][] = [ ["createdAt", "Date"], ]; -type SendgridRequest = (data: ClientRequest) => Promise; - -// TODO: When creating Sendgrid app, move this to the corresponding file -class Sendgrid { - constructor() { - if (!process.env.SENDGRID_API_KEY) throw Error("Sendgrid Api Key not present"); - sendgrid.setApiKey(process.env.SENDGRID_API_KEY); - return sendgrid; - } -} - const serviceName = "sendgrid_service"; export default class SendgridService extends SyncServiceCore implements ISyncService { + protected declare service: Sendgrid; constructor() { super(serviceName, Sendgrid, logger.getChildLogger({ prefix: [`[[sync] ${serviceName}`] })); } - sendgridRequest: SendgridRequest = async (data: ClientRequest) => { - this.log.debug("sendgridRequest:request", data); - const results = await this.service.request(data); - this.log.debug("sendgridRequest:results", results); - if (results[1].errors) throw Error(`Sendgrid request error: ${results[1].errors}`); - return results[1]; - }; - - getSendgridContactId = async (email: string) => { - const search = await this.sendgridRequest({ - url: `/v3/marketing/contacts/search`, - method: "POST", - body: { - query: `email LIKE '${email}'`, - }, - }); - this.log.debug("sync:sendgrid:getSendgridContactId:search", search); - return search.result || []; - }; - - getSendgridCustomFieldsIds = async () => { - // Get Custom Activity Fields - const allFields = await this.sendgridRequest({ - url: `/v3/marketing/field_definitions`, - method: "GET", - }); - allFields.custom_fields = allFields.custom_fields ?? []; - this.log.debug("sync:sendgrid:getCustomFieldsIds:allFields", allFields); - const customFieldsNames = allFields.custom_fields.map((fie) => fie.name); - this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsNames", customFieldsNames); - const customFieldsExist = calComCustomContactFields.map((cusFie) => - customFieldsNames.includes(cusFie[0]) - ); - this.log.debug("sync:sendgrid:getCustomFieldsIds:customFieldsExist", customFieldsExist); - return await Promise.all( - customFieldsExist.map(async (exist, idx) => { - if (!exist) { - const [name, field_type] = calComCustomContactFields[idx]; - const created = await this.sendgridRequest({ - url: `/v3/marketing/field_definitions`, - method: "POST", - body: { - name, - field_type, - }, - }); - this.log.debug("sync:sendgrid:getCustomFieldsIds:customField:created", created); - return created.id; - } else { - const index = customFieldsNames.findIndex((val) => val === calComCustomContactFields[idx][0]); - if (index >= 0) { - this.log.debug( - "sync:sendgrid:getCustomFieldsIds:customField:existed", - allFields.custom_fields[index].id - ); - return allFields.custom_fields[index].id; - } else { - throw Error("Couldn't find the field index"); - } - } - }) - ); - }; - upsert = async (user: WebUserInfoType | ConsoleUserInfoType) => { this.log.debug("sync:sendgrid:user", user); // Get Custom Contact fields ids - const customFieldsIds = await this.getSendgridCustomFieldsIds(); + const customFieldsIds = await this.service.getSendgridCustomFieldsIds(calComCustomContactFields); this.log.debug("sync:sendgrid:user:customFieldsIds", customFieldsIds); const lastBooking = "email" in user ? await this.getUserLastBooking(user) : null; this.log.debug("sync:sendgrid:user:lastBooking", lastBooking); @@ -159,7 +55,7 @@ export default class SendgridService extends SyncServiceCore implements ISyncSer ), }; this.log.debug("sync:sendgrid:contact:contactData", contactData); - const newContact = await this.sendgridRequest({ + const newContact = await this.service.sendgridRequest({ url: `/v3/marketing/contacts`, method: "PUT", body: { @@ -185,9 +81,9 @@ export default class SendgridService extends SyncServiceCore implements ISyncSer return this.upsert(webUser); }, delete: async (webUser: WebUserInfoType) => { - const [contactId] = await this.getSendgridContactId(webUser.email); + const [contactId] = await this.service.getSendgridContactId(webUser.email); if (contactId) { - return this.sendgridRequest({ + return this.service.sendgridRequest({ url: `/v3/marketing/contacts`, method: "DELETE", qs: { diff --git a/packages/prisma/seed-app-store.config.json b/packages/prisma/seed-app-store.config.json index 4b468c0f02..0c8594a081 100644 --- a/packages/prisma/seed-app-store.config.json +++ b/packages/prisma/seed-app-store.config.json @@ -101,5 +101,11 @@ "categories": ["video"], "slug": "sirius_video", "type": "sirius_video_video" + }, + { + "dirName": "sendgrid", + "categories": ["other"], + "slug": "sendgrid", + "type": "sendgrid_other_calendar" } ] diff --git a/packages/ui/v2/core/apps/AppCard.tsx b/packages/ui/v2/core/apps/AppCard.tsx index 62151fd8dc..9f4986271f 100644 --- a/packages/ui/v2/core/apps/AppCard.tsx +++ b/packages/ui/v2/core/apps/AppCard.tsx @@ -18,9 +18,10 @@ export default function AppCard({ app, credentials }: AppCardProps) { const { t } = useLocale(); const router = useRouter(); const mutation = useAddAppMutation(null, { - onSuccess: () => { + onSuccess: (data) => { // Refresh SSR page content without actual reload router.replace(router.asPath); + if (data.setupPending) return; showToast(t("app_successfully_installed"), "success"); }, onError: (error) => { diff --git a/turbo.json b/turbo.json index eb7071d799..2a23beb910 100644 --- a/turbo.json +++ b/turbo.json @@ -182,6 +182,7 @@ "$CI", "$CLOSECOM_API_KEY", "$SENDGRID_API_KEY", + "$SENDGRID_SYNC_API_KEY", "$SENDGRID_EMAIL", "$TWILIO_TOKEN", "$TWILIO_SID",