diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index f0f54e8d07..009a96f0e8 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -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={} success={({ data }) => { if (!data.items.length) { + const emptyHeaderCategory = getAppCategoryTitle(variant || "other", true); + return ( { 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", diff --git a/packages/app-store/_utils/getAppCategoryTitle.ts b/packages/app-store/_utils/getAppCategoryTitle.ts new file mode 100644 index 0000000000..25eb73cc08 --- /dev/null +++ b/packages/app-store/_utils/getAppCategoryTitle.ts @@ -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; diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 8192db1d33..73a9292609 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -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) { diff --git a/packages/app-store/closecom/api/_postAdd.ts b/packages/app-store/closecom/api/_postAdd.ts index f1fff19895..8c96fd57d4 100644 --- a/packages/app-store/closecom/api/_postAdd.ts +++ b/packages/app-store/closecom/api/_postAdd.ts @@ -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 { diff --git a/packages/app-store/closecom/config.json b/packages/app-store/closecom/config.json index 5d2c596abe..58a46a86e9 100644 --- a/packages/app-store/closecom/config.json +++ b/packages/app-store/closecom/config.json @@ -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", diff --git a/packages/app-store/hubspot/_metadata.ts b/packages/app-store/hubspot/_metadata.ts index c2aaa8deb7..ea5379a233 100644 --- a/packages/app-store/hubspot/_metadata.ts +++ b/packages/app-store/hubspot/_metadata.ts @@ -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/", diff --git a/packages/app-store/hubspot/api/add.ts b/packages/app-store/hubspot/api/add.ts index 9d205e01fb..1e8d6a4f52 100644 --- a/packages/app-store/hubspot/api/add.ts +++ b/packages/app-store/hubspot/api/add.ts @@ -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, diff --git a/packages/app-store/hubspot/api/callback.ts b/packages/app-store/hubspot/api/callback.ts index c43253fa69..14016534b8 100644 --- a/packages/app-store/hubspot/api/callback.ts +++ b/packages/app-store/hubspot/api/callback.ts @@ -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( diff --git a/packages/app-store/pipedrive-crm/config.json b/packages/app-store/pipedrive-crm/config.json index ff390101ba..013e361fa9 100644 --- a/packages/app-store/pipedrive-crm/config.json +++ b/packages/app-store/pipedrive-crm/config.json @@ -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", diff --git a/packages/app-store/salesforce/api/add.ts b/packages/app-store/salesforce/api/add.ts index 907afc723a..2d1ac45930 100644 --- a/packages/app-store/salesforce/api/add.ts +++ b/packages/app-store/salesforce/api/add.ts @@ -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({ diff --git a/packages/app-store/salesforce/api/callback.ts b/packages/app-store/salesforce/api/callback.ts index 7c9db80278..aad019a560 100644 --- a/packages/app-store/salesforce/api/callback.ts +++ b/packages/app-store/salesforce/api/callback.ts @@ -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( diff --git a/packages/app-store/salesforce/config.json b/packages/app-store/salesforce/config.json index 5495002b97..3f95a3c9b0 100644 --- a/packages/app-store/salesforce/config.json +++ b/packages/app-store/salesforce/config.json @@ -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", diff --git a/packages/app-store/sendgrid/config.json b/packages/app-store/sendgrid/config.json index 19d911f770..3f15a92402 100644 --- a/packages/app-store/sendgrid/config.json +++ b/packages/app-store/sendgrid/config.json @@ -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.", diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index a358f9d03a..7947f843ed 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -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; diff --git a/packages/app-store/zoho-bigin/config.json b/packages/app-store/zoho-bigin/config.json index 1f11350698..aea58ef2c7 100644 --- a/packages/app-store/zoho-bigin/config.json +++ b/packages/app-store/zoho-bigin/config.json @@ -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.", diff --git a/packages/app-store/zohocrm/api/callback.ts b/packages/app-store/zohocrm/api/callback.ts index ff53b2d3ee..9f02f28201 100644 --- a/packages/app-store/zohocrm/api/callback.ts +++ b/packages/app-store/zohocrm/api/callback.ts @@ -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( diff --git a/packages/app-store/zohocrm/config.json b/packages/app-store/zohocrm/config.json index 4166934341..daea3c1162 100644 --- a/packages/app-store/zohocrm/config.json +++ b/packages/app-store/zohocrm/config.json @@ -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", diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 8bae88b38c..9ec0373680 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -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); diff --git a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts index 1a4af97b41..8f612fad27 100644 --- a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts @@ -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) { diff --git a/packages/types/App.d.ts b/packages/types/App.d.ts index 39d9d5f70c..4c81987fc6 100644 --- a/packages/types/App.d.ts +++ b/packages/types/App.d.ts @@ -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;