refactor: Add crm type to app credentials (#14147)

* fix timezone display on booking page to reflect event availability timezone

* migrate fetching event owner's schedule to server side

* migrate fetching event owner's schedule to server side

* fix e2e test errors

* Add WEBAPP_URL_FOR_OAUTH to salesforce auth

* In event manager constructor include "_crm" credentials as calendar creds

* Change crm apps to type to end with `_crm`

* Move sendgrid out of CRM

* Add zoho bigin to CRM apps

* When getting apps, use slug

* Add `crm` variants

* Hubspot Oauth use `WEBAPP_URL_FOR_OAUTH`

* Refactor creating credentials

* Fix empty CRM page

* Use credentials with `_crm`

* Abstract getAppCategoryTitle

* Add integration.handler changes

* Fix tests

* Type fix

---------

Co-authored-by: Shaik-Sirajuddin <sirajuddinshaik30gmail.com>
Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Shaik-Sirajuddin <89742297+Shaik-Sirajuddin@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2024-04-09 16:46:06 +00:00
1 parent a1a2a141e4
commit 4a88b33782
21 files changed
+96 -44

No files matched your search

+4 -1
View File
@@ -2,6 +2,7 @@
import { useReducer } from "react";
import getAppCategoryTitle from "@calcom/app-store/_utils/getAppCategoryTitle";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -57,11 +58,13 @@ const IntegrationsContainer = ({
customLoader={<SkeletonLoader />}
success={({ data }) => {
if (!data.items.length) {
const emptyHeaderCategory = getAppCategoryTitle(variant || "other", true);
return (
<EmptyScreen
Icon={emptyIcon[variant || "other"]}
headline={t("no_category_apps", {
category: (variant && t(variant).toLowerCase()) || t("other").toLowerCase(),
category: emptyHeaderCategory,
})}
description={t(`no_category_apps_description_${variant || "other"}`)}
buttonRaw={
+9
View File
@@ -17,6 +17,7 @@ test.describe("Payment app", () => {
await prisma.credential.create({
data: {
type: "alby_payment",
appId: "alby",
userId: user.id,
key: {
account_id: "random",
@@ -57,6 +58,7 @@ test.describe("Payment app", () => {
await prisma.credential.create({
data: {
type: "stripe_payment",
appId: "stripe",
userId: user.id,
key: {
scope: "read_write",
@@ -101,6 +103,7 @@ test.describe("Payment app", () => {
await prisma.credential.create({
data: {
type: "paypal_payment",
appId: "paypal",
userId: user.id,
key: {
client_id: "randomString",
@@ -147,6 +150,7 @@ test.describe("Payment app", () => {
await prisma.credential.create({
data: {
type: "alby_payment",
appId: "alby",
userId: user.id,
key: {},
},
@@ -173,6 +177,7 @@ test.describe("Payment app", () => {
await prisma.credential.create({
data: {
type: "paypal_payment",
appId: "paypal",
userId: user.id,
key: {},
},
@@ -234,6 +239,7 @@ test.describe("Payment app", () => {
data: [
{
type: "paypal_payment",
appId: "paypal",
userId: user.id,
key: {
client_id: "randomString",
@@ -243,6 +249,7 @@ test.describe("Payment app", () => {
},
{
type: "stripe_payment",
appId: "stripe",
userId: user.id,
key: {
scope: "read_write",
@@ -279,6 +286,7 @@ test.describe("Payment app", () => {
data: [
{
type: "paypal_payment",
appId: "paypal",
userId: user.id,
key: {
client_id: "randomString",
@@ -288,6 +296,7 @@ test.describe("Payment app", () => {
},
{
type: "stripe_payment",
appId: "stripe",
userId: user.id,
key: {
scope: "read_write",
@@ -0,0 +1,22 @@
import type { AppCategories } from "@calcom/prisma/enums";
/**
* Handles if the app category should be full capitalized ex. CRM
*
* @param {App["variant"]} variant - The variant of the app.
* @param {boolean} [returnLowerCase] - Optional flag to return the title in lowercase.
*/
const getAppCategoryTitle = (variant: AppCategories, returnLowerCase?: boolean) => {
let title: string;
if (variant === "crm") {
title = "CRM";
return title;
} else {
title = variant;
}
return returnLowerCase ? title.toLowerCase() : title;
};
export default getAppCategoryTitle;
+5
View File
@@ -29,6 +29,11 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise
if (calendarType?.endsWith("_other_calendar")) {
calendarType = calendarType.split("_other_calendar")[0];
}
// Backwards compatibility until CRM manager is created
if (calendarType?.endsWith("_crm")) {
calendarType = calendarType.split("_crm")[0];
}
const calendarAppImportFn = appStore[calendarType.split("_").join("") as keyof typeof appStore];
if (!calendarAppImportFn) {
+3 -2
View File
@@ -8,6 +8,7 @@ import prisma from "@calcom/prisma";
import checkSession from "../../_utils/auth";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import appConfig from "../config.json";
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const session = checkSession(req);
@@ -18,10 +19,10 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || "");
const data = {
type: "closecom_other_calendar",
type: appConfig.type,
key: { encrypted },
userId: session.user?.id,
appId: "closecom",
appId: appConfig.slug,
};
try {
+2 -2
View File
@@ -3,10 +3,10 @@
"name": "Close.com",
"title": "Close.com",
"slug": "closecom",
"type": "closecom_other_calendar",
"type": "closecom_crm",
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "other",
"variant": "crm",
"categories": ["crm"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
+2 -2
View File
@@ -6,8 +6,8 @@ export const metadata = {
name: "HubSpot CRM",
installed: !!process.env.HUBSPOT_CLIENT_ID,
description: _package.description,
type: "hubspot_other_calendar",
variant: "other_calendar",
type: "hubspot_crm",
variant: "crm",
logo: "icon.svg",
publisher: "Cal.com",
url: "https://hubspot.com/",
+2 -2
View File
@@ -1,7 +1,7 @@
import * as hubspot from "@hubspot/api-client";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
const redirectUri = `${WEBAPP_URL}/api/integrations/hubspot/callback`;
const redirectUri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/hubspot/callback`;
const url = hubspotClient.oauth.getAuthorizationUrl(
client_id,
redirectUri,
+4 -3
View File
@@ -2,13 +2,14 @@ import * as hubspot from "@hubspot/api-client";
import type { TokenResponseIF } from "@hubspot/api-client/lib/codegen/oauth/models/TokenResponseIF";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import metadata from "../_metadata";
let client_id = "";
let client_secret = "";
@@ -39,7 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
"authorization_code",
code,
`${WEBAPP_URL}/api/integrations/hubspot/callback`,
`${WEBAPP_URL_FOR_OAUTH}/api/integrations/hubspot/callback`,
client_id,
client_secret
);
@@ -47,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// set expiry date as offset from current time.
hubspotToken.expiryDate = Math.round(Date.now() + hubspotToken.expiresIn * 1000);
await createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken, req);
await createOAuthAppCredential({ appId: metadata.slug, type: metadata.type }, hubspotToken, req);
const state = decodeOAuthState(req);
res.redirect(
+1 -1
View File
@@ -2,7 +2,7 @@
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Pipedrive CRM",
"slug": "pipedrive-crm",
"type": "pipedrive-crm_other_calendar",
"type": "pipedrive-crm_crm",
"logo": "icon.svg",
"url": "https://revert.dev",
"variant": "crm",
+2 -2
View File
@@ -1,7 +1,7 @@
import jsforce from "jsforce";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
@@ -17,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const salesforceClient = new jsforce.Connection({
clientId: consumer_key,
redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
redirectUri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/salesforce/callback`,
});
const url = salesforceClient.oauth2.getAuthorizationUrl({
@@ -1,13 +1,14 @@
import jsforce from "jsforce";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import appConfig from "../config.json";
let consumer_key = "";
let consumer_secret = "";
@@ -33,16 +34,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const conn = new jsforce.Connection({
clientId: consumer_key,
clientSecret: consumer_secret,
redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
redirectUri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/salesforce/callback`,
});
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
await createOAuthAppCredential(
{ appId: "salesforce", type: "salesforce_other_calendar" },
salesforceTokenInfo,
req
);
await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, salesforceTokenInfo, req);
const state = decodeOAuthState(req);
res.redirect(
+2 -2
View File
@@ -2,10 +2,10 @@
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Salesforce",
"slug": "salesforce",
"type": "salesforce_other_calendar",
"type": "salesforce_crm",
"logo": "icon.png",
"url": "https://cal.com/",
"variant": "other_calendar",
"variant": "crm",
"categories": ["crm"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
+1 -1
View File
@@ -6,7 +6,7 @@
"logo": "logo.png",
"url": "https://cal.com/",
"variant": "other_calendar",
"categories": ["crm"],
"categories": ["other"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.",
+1 -1
View File
@@ -47,7 +47,7 @@ export const ALL_APPS = Object.values(ALL_APPS_MAP);
*/
function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) {
const apps = ALL_APPS.reduce((reducedArray, appMeta) => {
const appCredentials = credentials.filter((credential) => credential.type === appMeta.type);
const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug);
if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray;
+3 -3
View File
@@ -2,11 +2,11 @@
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Zoho Bigin",
"slug": "zoho-bigin",
"type": "zoho-bigin_other_calendar",
"type": "zoho-bigin_crm",
"logo": "zohobigin.svg",
"url": "https://github.com/ShaneMaglangit",
"variant": "other",
"categories": ["other"],
"variant": "crm",
"categories": ["crm"],
"publisher": "Shane Maglangit",
"email": "help@cal.com",
"description": "Bigin easily transforms your day-to-day customer processes into actionable pipelines. From qualifying leads to closing deals to managing important after-sales operations—Bigin connects your different teams to work together so that you can offer the best possible experience to your customers. Say goodbye to missing follow-ups, manual data entry, lack of team communication, and information silos.",
+2 -5
View File
@@ -9,6 +9,7 @@ import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import appConfig from "../config.json";
let client_id = "";
let client_secret = "";
@@ -51,11 +52,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
zohoCrmTokenInfo.data.accountServer = req.query["accounts-server"];
await createOAuthAppCredential(
{ appId: "zohocrm", type: "zohocrm_other_calendar" },
zohoCrmTokenInfo.data,
req
);
await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, zohoCrmTokenInfo.data, req);
const state = decodeOAuthState(req);
res.redirect(
+2 -2
View File
@@ -2,10 +2,10 @@
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "ZohoCRM",
"slug": "zohocrm",
"type": "zohocrm_other_calendar",
"type": "zohocrm_crm",
"logo": "icon.svg",
"url": "https://github.com/jatinsandilya",
"variant": "other",
"variant": "crm",
"categories": ["crm"],
"publisher": "Jatin Sandilya",
"email": "help@cal.com",
+12 -4
View File
@@ -95,7 +95,10 @@ export default class EventManager {
// This includes all calendar-related apps, traditional calendars such as Google Calendar
// (type google_calendar) and non-traditional calendars such as CRMs like Close.com
// (type closecom_other_calendar)
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
this.calendarCredentials = appCredentials.filter(
// Backwards compatibility until CRM manager is implemented
(cred) => cred.type.endsWith("_calendar") || cred.type.endsWith("_crm")
);
this.videoCredentials = appCredentials
.filter((cred) => cred.type.endsWith("_video") || cred.type.endsWith("_conferencing"))
// Whenever a new video connection is added, latest credentials are added with the highest ID.
@@ -518,7 +521,10 @@ export default class EventManager {
* Not ideal but, if we don't find a destination calendar,
* fallback to the first connected calendar - Shouldn't be a CRM calendar
*/
const [credential] = this.calendarCredentials.filter((cred) => !cred.type.endsWith("other_calendar"));
// Backwards compatibility until CRM manager is created
const [credential] = this.calendarCredentials.filter(
(cred) => !cred.type.endsWith("other_calendar") || !cred.type.endsWith("crm")
);
if (credential) {
const createdEvent = await createEvent(credential, event);
log.silly("Created Calendar event", safeStringify({ createdEvent }));
@@ -618,7 +624,8 @@ export default class EventManager {
createdEvents = createdEvents.concat(
await Promise.all(
this.calendarCredentials
.filter((cred) => cred.type.includes("other_calendar"))
// Backwards compatibility until CRM manager is created
.filter((cred) => cred.type.includes("other_calendar") || cred.type.includes("crm"))
.map(async (cred) => await createEvent(cred, event))
)
);
@@ -790,7 +797,8 @@ export default class EventManager {
// Taking care of non-traditional calendar integrations
result = result.concat(
this.calendarCredentials
.filter((cred) => cred.type.includes("other_calendar"))
// Backwards compatibility until CRM manager is created
.filter((cred) => cred.type.includes("other_calendar") && cred.type.includes("crm"))
.map(async (cred) => {
const calendarReference = booking.references.find((ref) => ref.type === cred.type);
@@ -135,13 +135,13 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
//TODO: Refactor this to pick up only needed fields and prevent more leaking
let apps = await Promise.all(
enabledApps.map(async ({ credentials: _, credential, key: _2 /* don't leak to frontend */, ...app }) => {
const userCredentialIds = credentials.filter((c) => c.type === app.type && !c.teamId).map((c) => c.id);
const userCredentialIds = credentials.filter((c) => c.appId === app.slug && !c.teamId).map((c) => c.id);
const invalidCredentialIds = credentials
.filter((c) => c.type === app.type && c.invalid)
.filter((c) => c.appId === app.slug && c.invalid)
.map((c) => c.id);
const teams = await Promise.all(
credentials
.filter((c) => c.type === app.type && c.teamId)
.filter((c) => c.appId === app.slug && c.teamId)
.map(async (c) => {
const team = userTeams.find((team) => team.id === c.teamId);
if (!team) {
+10 -1
View File
@@ -66,6 +66,7 @@ export interface App {
| `${string}_other`
| `${string}_automation`
| `${string}_analytics`
| `${string}_crm`
| `${string}_other_calendar`;
/**
@@ -79,7 +80,15 @@ export interface App {
/** A brief description, usually found in the app's package.json */
description: string;
/** TODO determine if we should use this instead of category */
variant: "calendar" | "payment" | "conferencing" | "video" | "other" | "other_calendar" | "automation";
variant:
| "calendar"
| "payment"
| "conferencing"
| "video"
| "other"
| "other_calendar"
| "automation"
| "crm";
/** The slug for the app store public page inside `/apps/[slug] */
slug: string;