Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f557f7f9 | ||
|
|
e2921c3126 | ||
|
|
d8102615b1 | ||
|
|
1010e2894a | ||
|
|
c728174c14 | ||
|
|
3889b49fa1 | ||
|
|
461389fb0b | ||
|
|
a8186e3039 | ||
|
|
5e2db12396 | ||
|
|
6bc9d2fd94 | ||
|
|
37f9bbf3fa | ||
|
|
fb5b3bbcc6 | ||
|
|
d43ee154d0 |
+3
-3
@@ -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=
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
@@ -8,4 +9,8 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: curl -X POST -d {} ${{ secrets.VERCEL_VERSIONED_DEPLOY_HOOK }}
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- run: git push origin +main:production
|
||||
|
||||
@@ -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": ""
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -173,7 +173,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 pt-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="flex w-full items-end">
|
||||
<div className="flex w-full items-end justify-end">
|
||||
<Controller
|
||||
name="minimumBookingNotice"
|
||||
control={formMethods.control}
|
||||
@@ -201,7 +201,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
label={t("minimum_booking_notice")}
|
||||
type="number"
|
||||
placeholder="120"
|
||||
className="mr-2 rounded-[4px]"
|
||||
className="mr-2 mb-0 h-[38px] rounded-[4px]"
|
||||
{...formMethods.register("minimumBookingNoticeInDurationType", {
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onMinimumNoticeChange(event.target.value),
|
||||
@@ -211,7 +211,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
</div>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
className="mb-2 ml-2 w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||
defaultValue={durationTypeOptions.find(
|
||||
(option) => option.value === minimumBookingNoticeType.current
|
||||
)}
|
||||
@@ -234,7 +234,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
||||
label: t("slot_interval_default"),
|
||||
value: -1,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
|
||||
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
|
||||
label: minutes + " " + t("minutes"),
|
||||
value: minutes,
|
||||
})),
|
||||
|
||||
+11
-7
@@ -80,6 +80,7 @@ plugins.push(withAxiom);
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
i18n,
|
||||
productionBrowserSourceMaps: true,
|
||||
/* We already do type check on GH actions */
|
||||
typescript: {
|
||||
ignoreBuildErrors: !!process.env.CI,
|
||||
@@ -88,10 +89,8 @@ const nextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: !!process.env.CI,
|
||||
},
|
||||
experimental: {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.plugins.push(
|
||||
@@ -230,9 +229,6 @@ const nextConfig = {
|
||||
|
||||
return redirects;
|
||||
},
|
||||
sentry: {
|
||||
hideSourceMaps: true,
|
||||
},
|
||||
};
|
||||
|
||||
const sentryWebpackPluginOptions = {
|
||||
@@ -241,6 +237,14 @@ const sentryWebpackPluginOptions = {
|
||||
|
||||
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
nextConfig.sentry = {
|
||||
hideSourceMaps: true,
|
||||
// Prevents Sentry from running on this Edge function, where Sentry doesn't work yet (build whould crash the api route).
|
||||
excludeServerRoutes: [/\/api\/social\/og\/image\/?/],
|
||||
};
|
||||
}
|
||||
|
||||
// Sentry should be the last thing to export to catch everything right
|
||||
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -56,7 +56,7 @@
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-toggle-group": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"@sentry/nextjs": "^7.17.3",
|
||||
"@sentry/nextjs": "^7.20.0",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
|
||||
@@ -179,7 +179,9 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
||||
})}
|
||||
description={t(`no_category_apps_description_${variant || "other"}`)}
|
||||
buttonRaw={
|
||||
<Button color="secondary" href={variant ? `/apps/categories/${variant}` : "/apps"}>
|
||||
<Button
|
||||
color="secondary"
|
||||
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
|
||||
{t(`connect_${variant || "other"}_apps`)}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||
return (
|
||||
<Link href={`/event-types/${type.id}?tabName=setup`}>
|
||||
<a
|
||||
className="w-full overflow-hidden pr-4 text-sm"
|
||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||
<div>
|
||||
<span
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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<typeof useMutation>[2]) {
|
||||
type CustomUseMutationOptions =
|
||||
| Omit<UseMutationOptions<unknown, unknown, unknown, unknown>, "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<typeo
|
||||
isOmniInstall = variables.isOmniInstall;
|
||||
type = variables.type;
|
||||
}
|
||||
if (type === "sendgrid_other_calendar") {
|
||||
type = "sendgrid";
|
||||
}
|
||||
const state: IntegrationOAuthCallbackState = {
|
||||
returnTo:
|
||||
WEBAPP_URL +
|
||||
@@ -47,20 +58,22 @@ function useAddAppMutation(_type: App["type"] | null, options?: Parameters<typeo
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const externalUrl = /https?:\/\//.test(json.url) && !json.url.startsWith(window.location.origin);
|
||||
|
||||
if (!isOmniInstall) {
|
||||
gotoUrl(json.url, json.newTab);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip redirection only if it is an OmniInstall and redirect URL isn't of some other origin
|
||||
// This allows installation of apps like Stripe to still redirect to their authentication pages.
|
||||
|
||||
// Check first that the URL is absolute, then check that it is of different origin from the current.
|
||||
if (/https?:\/\//.test(json.url) && !json.url.startsWith(window.location.origin)) {
|
||||
if (externalUrl) {
|
||||
// TODO: For Omni installation to authenticate and come back to the page where installation was initiated, some changes need to be done in all apps' add callbacks
|
||||
gotoUrl(json.url, json.newTab);
|
||||
}
|
||||
|
||||
return { setupPending: !externalUrl && json.url.endsWith("/setup") };
|
||||
}, options);
|
||||
|
||||
return mutation;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { metadata as qr_code_meta } from "./qr_code/_metadata";
|
||||
import { metadata as rainbow_meta } from "./rainbow/_metadata";
|
||||
import { metadata as raycast_meta } from "./raycast/_metadata";
|
||||
import { metadata as riverside_meta } from "./riverside/_metadata";
|
||||
import { metadata as sendgrid_meta } from "./sendgrid/_metadata";
|
||||
import { metadata as sirius_video_meta } from "./sirius_video/_metadata";
|
||||
import { metadata as stripepayment_meta } from "./stripepayment/_metadata";
|
||||
import { metadata as tandemvideo_meta } from "./tandemvideo/_metadata";
|
||||
@@ -70,6 +71,7 @@ export const appStoreMetadata = {
|
||||
rainbow: rainbow_meta,
|
||||
raycast: raycast_meta,
|
||||
riverside: riverside_meta,
|
||||
sendgrid: sendgrid_meta,
|
||||
sirius_video: sirius_video_meta,
|
||||
stripepayment: stripepayment_meta,
|
||||
tandemvideo: tandemvideo_meta,
|
||||
|
||||
@@ -29,6 +29,7 @@ export const apiHandlers = {
|
||||
rainbow: import("./rainbow/api"),
|
||||
raycast: import("./raycast/api"),
|
||||
riverside: import("./riverside/api"),
|
||||
sendgrid: import("./sendgrid/api"),
|
||||
sirius_video: import("./sirius_video/api"),
|
||||
stripepayment: import("./stripepayment/api"),
|
||||
tandemvideo: import("./tandemvideo/api"),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
jest.mock("@calcom/lib/logger", () => ({
|
||||
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", () => ({
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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" })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
export default defaultHandler({
|
||||
GET: import("./_getAdd"),
|
||||
POST: import("./_postAdd"),
|
||||
});
|
||||
@@ -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) }),
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as check } from "./check";
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
export { metadata } from "./_metadata";
|
||||
@@ -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<NewCalendarEventType> {
|
||||
// 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<SendgridNewContact>({
|
||||
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<any> {
|
||||
// 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<void> {
|
||||
// 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<EventBusyDate[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CalendarService } from "./CalendarService";
|
||||
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
@@ -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<boolean | undefined>(undefined);
|
||||
const [testLoading, setTestLoading] = useState<boolean>(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 (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[520px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
|
||||
<div>
|
||||
<img src="/api/app-store/sendgrid/logo.png" alt="Sendgrid" className="h-12 w-12 max-w-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{t("generate_api_key_description")}{" "}
|
||||
<a
|
||||
className="text-indigo-400"
|
||||
href="https://app.sendgrid.com/settings/api_keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Sendgrid
|
||||
</a>
|
||||
. {t("it_stored_encrypted")}
|
||||
</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
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");
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<Controller
|
||||
name="api_key"
|
||||
control={form.control}
|
||||
render={({ field: { onBlur, onChange } }) => (
|
||||
<TextField
|
||||
className="my-0"
|
||||
onBlur={onBlur}
|
||||
disabled={testPassed === true}
|
||||
name="api_key"
|
||||
placeholder="api_xyz..."
|
||||
onChange={async (e) => {
|
||||
onChange(e.target.value);
|
||||
form.setValue("api_key", e.target.value);
|
||||
await form.trigger("api_key");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={testLoading}
|
||||
disabled={testPassed === true}
|
||||
StartIcon={testPassed !== undefined ? (testPassed ? Icon.FiCheck : Icon.FiX) : undefined}
|
||||
className={
|
||||
testPassed !== undefined
|
||||
? testPassed
|
||||
? " !bg-green-100 !text-green-700 hover:bg-green-100"
|
||||
: "!border-red-700 bg-red-100 !text-red-700 hover:bg-red-100"
|
||||
: "secondary"
|
||||
}
|
||||
color={testPassed === true ? "minimal" : "secondary"}
|
||||
onClick={async () => {
|
||||
const check = await form.trigger("api_key");
|
||||
if (!check) return;
|
||||
const api_key = form.getValues("api_key");
|
||||
setTestLoading(true);
|
||||
const res = await fetch("/api/integrations/sendgrid/check", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ api_key }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
setTestPassed(true);
|
||||
} else {
|
||||
setTestPassed(false);
|
||||
}
|
||||
setTestLoading(false);
|
||||
}}>
|
||||
{t(
|
||||
testPassed !== undefined ? (testPassed ? "test_passed" : "test_failed") : "test_api_key"
|
||||
)}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -105,7 +105,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
||||
<li>You're set!</li>
|
||||
</Trans>
|
||||
</ol>
|
||||
<Link href="/apps/installed" passHref={true}>
|
||||
<Link href="/apps/installed/automation?hl=zapier" passHref={true}>
|
||||
<Button color="secondary">{t("done")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -339,7 +339,10 @@ const handleZoomResponse = async (response: Response, credentialId: Credential["
|
||||
}
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
|
||||
// handle 204 response code with empty response (causes crash otherwise as "" is invalid JSON)
|
||||
if (response.status === 204) {
|
||||
return;
|
||||
}
|
||||
return responseClone.json();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { MembershipRole, Prisma } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
@@ -19,12 +21,19 @@ import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
interface TeamProfileValues {
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
bio: string;
|
||||
}
|
||||
const regex = new RegExp("^[a-zA-Z0-9-]*$");
|
||||
|
||||
const teamProfileFormSchema = z.object({
|
||||
name: z.string(),
|
||||
slug: z
|
||||
.string()
|
||||
.regex(regex, {
|
||||
message: "Url can only have alphanumeric characters(a-z, 0-9) and hyphen(-) symbol.",
|
||||
})
|
||||
.min(1, { message: "Url cannot be left empty" }),
|
||||
logo: z.string(),
|
||||
bio: z.string(),
|
||||
});
|
||||
|
||||
const ProfileView = () => {
|
||||
const { t } = useLocale();
|
||||
@@ -42,7 +51,9 @@ const ProfileView = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<TeamProfileValues>();
|
||||
const form = useForm({
|
||||
resolver: zodResolver(teamProfileFormSchema),
|
||||
});
|
||||
|
||||
const { data: team, isLoading } = trpc.viewer.teams.get.useQuery(
|
||||
{ teamId: Number(router.query.id) },
|
||||
@@ -53,11 +64,11 @@ const ProfileView = () => {
|
||||
onSuccess: (team) => {
|
||||
if (team) {
|
||||
form.setValue("name", team.name || "");
|
||||
form.setValue("url", team.slug || "");
|
||||
form.setValue("slug", team.slug || "");
|
||||
form.setValue("logo", team.logo || "");
|
||||
form.setValue("bio", team.bio || "");
|
||||
if (team.slug === null && (team?.metadata as Prisma.JsonObject)?.requestedSlug) {
|
||||
form.setValue("url", ((team?.metadata as Prisma.JsonObject)?.requestedSlug as string) || "");
|
||||
form.setValue("slug", ((team?.metadata as Prisma.JsonObject)?.requestedSlug as string) || "");
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -124,7 +135,7 @@ const ProfileView = () => {
|
||||
const variables = {
|
||||
logo: values.logo,
|
||||
name: values.name,
|
||||
slug: values.url,
|
||||
slug: values.slug,
|
||||
bio: values.bio,
|
||||
};
|
||||
objectKeys(variables).forEach((key) => {
|
||||
@@ -176,16 +187,17 @@ const ProfileView = () => {
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="url"
|
||||
name="slug"
|
||||
render={({ field: { value } }) => (
|
||||
<div className="mt-8">
|
||||
<TextField
|
||||
name="url"
|
||||
name="slug"
|
||||
label={t("team_url")}
|
||||
value={value}
|
||||
addOnLeading={`${WEBAPP_URL}/team/`}
|
||||
onChange={(e) => {
|
||||
form.setValue("url", e?.target.value);
|
||||
form.clearErrors("slug");
|
||||
form.setValue("slug", e?.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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<SendgridUsernameResult>({
|
||||
url: `/v3/user/username`,
|
||||
method: "GET",
|
||||
});
|
||||
return username;
|
||||
};
|
||||
|
||||
public async sendgridRequest<R = ClientResponse>(data: ClientRequest): Promise<R> {
|
||||
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<SendgridSearchResult>({
|
||||
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<SendgridFieldDefinitions>({
|
||||
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<SendgridCustomField>({
|
||||
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");
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,13 @@ export default function convertToNewDurationType(
|
||||
/** Convert `prevValue` from `prevType` to `newType` */
|
||||
const newDurationTypeMap = {
|
||||
minutes_minutes: () => prevValue,
|
||||
minutes_hours: () => prevValue * MINUTES_IN_HOUR,
|
||||
minutes_days: () => prevValue * MINUTES_IN_DAY,
|
||||
hours_minutes: () => prevValue / MINUTES_IN_HOUR,
|
||||
minutes_hours: () => prevValue / MINUTES_IN_HOUR,
|
||||
minutes_days: () => prevValue / MINUTES_IN_DAY,
|
||||
hours_minutes: () => prevValue * MINUTES_IN_HOUR,
|
||||
hours_hours: () => prevValue,
|
||||
hours_days: () => prevValue * HOURS_IN_DAY,
|
||||
days_minutes: () => prevValue / MINUTES_IN_DAY,
|
||||
days_hours: () => prevValue / HOURS_IN_DAY,
|
||||
days_minutes: () => prevValue * MINUTES_IN_DAY,
|
||||
days_hours: () => prevValue * HOURS_IN_DAY,
|
||||
days_days: () => prevValue,
|
||||
};
|
||||
const getNewValue = newDurationTypeMap[`${prevType}_${newType}`];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { performance } from "./perfObserver";
|
||||
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
|
||||
|
||||
/** Allows us to get type inference from API handler responses */
|
||||
function defaultResponder<T>(f: Handle<T>) {
|
||||
export function defaultResponder<T>(f: Handle<T>) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
let ok = false;
|
||||
try {
|
||||
@@ -25,5 +25,3 @@ function defaultResponder<T>(f: Handle<T>) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = <R = ClientResponse>(data: ClientRequest) => Promise<R>;
|
||||
|
||||
// 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<SendgridSearchResult>({
|
||||
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<SendgridFieldDefinitions>({
|
||||
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<SendgridCustomField>({
|
||||
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<SendgridNewContact>({
|
||||
const newContact = await this.service.sendgridRequest<SendgridNewContact>({
|
||||
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: {
|
||||
|
||||
@@ -101,5 +101,11 @@
|
||||
"categories": ["video"],
|
||||
"slug": "sirius_video",
|
||||
"type": "sirius_video_video"
|
||||
},
|
||||
{
|
||||
"dirName": "sendgrid",
|
||||
"categories": ["other"],
|
||||
"slug": "sendgrid",
|
||||
"type": "sendgrid_other_calendar"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"$CI",
|
||||
"$CLOSECOM_API_KEY",
|
||||
"$SENDGRID_API_KEY",
|
||||
"$SENDGRID_SYNC_API_KEY",
|
||||
"$SENDGRID_EMAIL",
|
||||
"$TWILIO_TOKEN",
|
||||
"$TWILIO_SID",
|
||||
|
||||
@@ -5033,6 +5033,16 @@
|
||||
"@sentry/utils" "7.17.4"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/browser@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.20.0.tgz#e917cab48d3bba2fe8a2e3a8987b0bd9b9149db8"
|
||||
integrity sha512-L84CdB7DPQ2ohVcWh/KivemndWSZyXRvBZBr+tHFlQchzcaZZ/8lIPvjwvb8OJhzhecDq6JCAyUxaZwyItdyAg==
|
||||
dependencies:
|
||||
"@sentry/core" "7.20.0"
|
||||
"@sentry/types" "7.20.0"
|
||||
"@sentry/utils" "7.20.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/cli@^1.74.6":
|
||||
version "1.74.6"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.6.tgz#c4f276e52c6f5e8c8d692845a965988068ebc6f5"
|
||||
@@ -5055,6 +5065,15 @@
|
||||
"@sentry/utils" "7.17.4"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/core@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.20.0.tgz#7becd848c982d5422627f69c4061d5914946c54a"
|
||||
integrity sha512-8dIHk8niyEyVayUQpgECHnV2p444nPBjIyuQrtkdTxL7sBLC5+Y0DhRjxg9cJyZe/bZnXVerGkgcA7niKW4W8A==
|
||||
dependencies:
|
||||
"@sentry/types" "7.20.0"
|
||||
"@sentry/utils" "7.20.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/integrations@7.17.4":
|
||||
version "7.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.17.4.tgz#facdc4abb1d4714175c7d66bab634a898cc7eead"
|
||||
@@ -5065,6 +5084,16 @@
|
||||
localforage "^1.8.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/integrations@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.20.0.tgz#78fc544847c5516acb86fc507d0bfdaedb295fd8"
|
||||
integrity sha512-wT00rtdKiVOZm/2fGEotIQ2ItCU9xTODz3KveQLq2X2nI5Jg4opkDOY2NorEnuOfBNg6BncBxaC8TG8ujvRyfw==
|
||||
dependencies:
|
||||
"@sentry/types" "7.20.0"
|
||||
"@sentry/utils" "7.20.0"
|
||||
localforage "^1.8.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/nextjs@^7.17.3":
|
||||
version "7.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.17.4.tgz#9c6d71d17e37da3fcbe753a374e93307a56dbf02"
|
||||
@@ -5084,6 +5113,25 @@
|
||||
rollup "2.78.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/nextjs@^7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.20.0.tgz#e30cdf43be4bf0bc955b4e8d3f8eacccada21900"
|
||||
integrity sha512-riPRp/+1awbOQ+TJXp1nmNKwvdtQrGFvTf1l6eONUbmIqN0ZWfToyEGYfFX1jbHamRHNvXcwKhixuPlJG+Nqyg==
|
||||
dependencies:
|
||||
"@rollup/plugin-sucrase" "4.0.4"
|
||||
"@rollup/plugin-virtual" "3.0.0"
|
||||
"@sentry/core" "7.20.0"
|
||||
"@sentry/integrations" "7.20.0"
|
||||
"@sentry/node" "7.20.0"
|
||||
"@sentry/react" "7.20.0"
|
||||
"@sentry/tracing" "7.20.0"
|
||||
"@sentry/types" "7.20.0"
|
||||
"@sentry/utils" "7.20.0"
|
||||
"@sentry/webpack-plugin" "1.20.0"
|
||||
chalk "3.0.0"
|
||||
rollup "2.78.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@7.17.4":
|
||||
version "7.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.17.4.tgz#647ebcbd7228e5fa326dc3f95aceab34771205d3"
|
||||
@@ -5097,6 +5145,19 @@
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.20.0.tgz#a32e37b2c93607e8b8d7f3741f60418ec2d99ddb"
|
||||
integrity sha512-aB0VSueCiWPg53DIevnKNYSu2OGsPweWO1eKhMhu1uJNbL+ZYAFRczdcLdkWCqa38X5jjqO82GKuIZYpXUGX+A==
|
||||
dependencies:
|
||||
"@sentry/core" "7.20.0"
|
||||
"@sentry/types" "7.20.0"
|
||||
"@sentry/utils" "7.20.0"
|
||||
cookie "^0.4.1"
|
||||
https-proxy-agent "^5.0.0"
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/react@7.17.4":
|
||||
version "7.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.17.4.tgz#1d2c752841ace0561043be6a863de9fc7437f64c"
|
||||
@@ -5108,6 +5169,17 @@
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/react@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.20.0.tgz#41e1edb5a7330bf15ab06e0c0b3e0be7cf36a7d3"
|
||||
integrity sha512-hXPObzl4I7TgeCGEb3b03yLI7zF/oYQ5NoGz65fmhUainOGuW+S8KSyCWmAvaHXZ1cZao+sLfQSxl29sVVQyww==
|
||||
dependencies:
|
||||
"@sentry/browser" "7.20.0"
|
||||
"@sentry/types" "7.20.0"
|
||||
"@sentry/utils" "7.20.0"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@7.17.4":
|
||||
version "7.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.17.4.tgz#1254f4eb7397e1134248de1dade33d2fec864005"
|
||||
@@ -5118,11 +5190,26 @@
|
||||
"@sentry/utils" "7.17.4"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.20.0.tgz#346bf72bc5574ac78de43bd8ab6f6027f8b64cbd"
|
||||
integrity sha512-qg3sMvjuMQl/NEaF8I2IpvUcJ4HGGVIwEqqqZ6hkeHXIKt02p6f+nls45pVhluMiNHAaQJ+vefMTUc3E1yZwDA==
|
||||
dependencies:
|
||||
"@sentry/core" "7.20.0"
|
||||
"@sentry/types" "7.20.0"
|
||||
"@sentry/utils" "7.20.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@7.17.4":
|
||||
version "7.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.17.4.tgz#476522bc988989101e7aee9eee3c3f8f16fa59ea"
|
||||
integrity sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ==
|
||||
|
||||
"@sentry/types@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.20.0.tgz#ace232e00407355dccedf65e27471635b4331265"
|
||||
integrity sha512-x17ddduGWqW95neBFVvxzmInb5WXVw+2PcNASHXpGFhi7v2gz2a7/w2CcIKxsqODNnc+z/k1t0Y+uy9B6aH6ag==
|
||||
|
||||
"@sentry/utils@7.17.4":
|
||||
version "7.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.17.4.tgz#b8f4fb6a329765b97668b34f6d397e8de2169fef"
|
||||
@@ -5131,6 +5218,14 @@
|
||||
"@sentry/types" "7.17.4"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/utils@7.20.0":
|
||||
version "7.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.20.0.tgz#cca09e2256b64bfb41a78c89af3c88fcc7ca84c2"
|
||||
integrity sha512-4lc122TFgkaCAvoPRy+uc5vgOCumTa/2nPkzCSxVsezQs+ebHxyMJQK7GWBLI6P+EzKfEjlgyMzRWaPJ3iJatA==
|
||||
dependencies:
|
||||
"@sentry/types" "7.20.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/webpack-plugin@1.20.0":
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58"
|
||||
|
||||
Reference in New Issue
Block a user