From ef572746177979aa8d7763aa70d57009983ba278 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Wed, 20 Aug 2025 02:39:46 +0800 Subject: [PATCH 01/22] perf: improve `/apps/installed/calendar` page load speed. --- .../_utils/calendars/calendarLoaders.ts | 16 ++++++++++++++++ packages/app-store/_utils/createCachedImport.ts | 10 ++++++++++ packages/app-store/_utils/getCalendar.ts | 8 +++++--- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 packages/app-store/_utils/calendars/calendarLoaders.ts create mode 100644 packages/app-store/_utils/createCachedImport.ts diff --git a/packages/app-store/_utils/calendars/calendarLoaders.ts b/packages/app-store/_utils/calendars/calendarLoaders.ts new file mode 100644 index 00000000000000..9886defff7f91c --- /dev/null +++ b/packages/app-store/_utils/calendars/calendarLoaders.ts @@ -0,0 +1,16 @@ +import { createCachedImport } from "../createCachedImport"; + +export const calendarLoaders = { + applecalendar: createCachedImport(() => import("../../applecalendar")), + caldavcalendar: createCachedImport(() => import("../../caldavcalendar")), + googlecalendar: createCachedImport(() => import("../../googlecalendar")), + "ics-feedcalendar": createCachedImport(() => import("../../ics-feedcalendar")), + larkcalendar: createCachedImport(() => import("../../larkcalendar")), + office365calendar: createCachedImport(() => import("../../office365calendar")), + exchange2013calendar: createCachedImport(() => import("../../exchange2013calendar")), + exchange2016calendar: createCachedImport(() => import("../../exchange2016calendar")), + exchangecalendar: createCachedImport(() => import("../../exchangecalendar")), + zohocalendar: createCachedImport(() => import("../../zohocalendar")), +}; + +export type CalendarLoaderKey = keyof typeof calendarLoaders; diff --git a/packages/app-store/_utils/createCachedImport.ts b/packages/app-store/_utils/createCachedImport.ts new file mode 100644 index 00000000000000..5fe483afe1432e --- /dev/null +++ b/packages/app-store/_utils/createCachedImport.ts @@ -0,0 +1,10 @@ +export function createCachedImport(importFunc: () => Promise): () => Promise { + let cachedModule: T | undefined; + + return async () => { + if (!cachedModule) { + cachedModule = await importFunc(); + } + return cachedModule; + }; +} diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 9fb865c203bd68..7e43e93ae4b28f 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -2,7 +2,8 @@ import logger from "@calcom/lib/logger"; import type { Calendar, CalendarClass } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; -import appStore from ".."; +import { calendarLoaders } from "./calendars/calendarLoaders"; +import type { CalendarLoaderKey } from "./calendars/calendarLoaders"; interface CalendarApp { lib: { @@ -36,14 +37,15 @@ export const getCalendar = async ( calendarType = calendarType.split("_crm")[0]; } - const calendarAppImportFn = appStore[calendarType.split("_").join("") as keyof typeof appStore]; + const calendarLoaderKey = calendarType.split("_").join("") as CalendarLoaderKey; + const calendarAppImportFn = calendarLoaders[calendarLoaderKey]; if (!calendarAppImportFn) { log.warn(`calendar of type ${calendarType} is not implemented`); return null; } - const calendarApp = await calendarAppImportFn(); + const calendarApp = await calendarAppImportFn?.(); if (!isCalendarService(calendarApp)) { log.warn(`calendar of type ${calendarType} is not implemented`); From 1c8aec9d4b8fa3891d643c7ccb3d8572de365c38 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Wed, 20 Aug 2025 17:21:53 +0800 Subject: [PATCH 02/22] feat: add analytics loaders. --- .../_utils/analytics/analyticsLoaders.ts | 8 +++++++ packages/app-store/_utils/getAnalytics.ts | 5 ++-- packages/app-store/index.ts | 24 +++++++++---------- 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 packages/app-store/_utils/analytics/analyticsLoaders.ts diff --git a/packages/app-store/_utils/analytics/analyticsLoaders.ts b/packages/app-store/_utils/analytics/analyticsLoaders.ts new file mode 100644 index 00000000000000..b4ef247c4e375d --- /dev/null +++ b/packages/app-store/_utils/analytics/analyticsLoaders.ts @@ -0,0 +1,8 @@ +import { createCachedImport } from "../createCachedImport"; + +export const analyticsLoaders = { + dub: createCachedImport(() => import("../../dub")), + plausible: createCachedImport(() => import("../../plausible")), +}; + +export type AnalyticsLoadersKey = keyof typeof analyticsLoaders; diff --git a/packages/app-store/_utils/getAnalytics.ts b/packages/app-store/_utils/getAnalytics.ts index e0a68180f673ea..bdf08f4d7f4921 100644 --- a/packages/app-store/_utils/getAnalytics.ts +++ b/packages/app-store/_utils/getAnalytics.ts @@ -2,7 +2,8 @@ import logger from "@calcom/lib/logger"; import type { AnalyticsService, AnalyticsServiceClass } from "@calcom/types/AnalyticsService"; import type { CredentialPayload } from "@calcom/types/Credential"; -import appStore from ".."; +import { analyticsLoaders } from "./analytics/analyticsLoaders"; +import type { AnalyticsLoadersKey } from "./analytics/analyticsLoaders"; const log = logger.getSubLogger({ prefix: ["AnalyticsManager"] }); @@ -30,7 +31,7 @@ export const getAnalyticsService = async ({ const analyticsName = analyticsType.split("_")[0]; - const analyticsAppImportFn = appStore[analyticsName as keyof typeof appStore]; + const analyticsAppImportFn = analyticsLoaders[analyticsName as AnalyticsLoadersKey]; if (!analyticsAppImportFn) { log.warn(`analytics app not implemented`); diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 8ddea439c5fe0d..dc4f8a34191d15 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -1,22 +1,22 @@ const appStore = { alby: createCachedImport(() => import("./alby")), - applecalendar: createCachedImport(() => import("./applecalendar")), - caldavcalendar: createCachedImport(() => import("./caldavcalendar")), + // applecalendar: createCachedImport(() => import("./applecalendar")), + // caldavcalendar: createCachedImport(() => import("./caldavcalendar")), closecom: createCachedImport(() => import("./closecom")), dailyvideo: createCachedImport(() => import("./dailyvideo")), - dub: createCachedImport(() => import("./dub")), - googlecalendar: createCachedImport(() => import("./googlecalendar")), + // dub: createCachedImport(() => import("./dub")), + // googlecalendar: createCachedImport(() => import("./googlecalendar")), googlevideo: createCachedImport(() => import("./googlevideo")), hubspot: createCachedImport(() => import("./hubspot")), huddle01video: createCachedImport(() => import("./huddle01video")), - "ics-feedcalendar": createCachedImport(() => import("./ics-feedcalendar")), + // "ics-feedcalendar": createCachedImport(() => import("./ics-feedcalendar")), jellyconferencing: createCachedImport(() => import("./jelly")), jitsivideo: createCachedImport(() => import("./jitsivideo")), - larkcalendar: createCachedImport(() => import("./larkcalendar")), + // larkcalendar: createCachedImport(() => import("./larkcalendar")), nextcloudtalkvideo: createCachedImport(() => import("./nextcloudtalk")), - office365calendar: createCachedImport(() => import("./office365calendar")), + // office365calendar: createCachedImport(() => import("./office365calendar")), office365video: createCachedImport(() => import("./office365video")), - plausible: createCachedImport(() => import("./plausible")), + // plausible: createCachedImport(() => import("./plausible")), paypal: createCachedImport(() => import("./paypal")), "pipedrive-crm": createCachedImport(() => import("./pipedrive-crm")), salesforce: createCachedImport(() => import("./salesforce")), @@ -31,12 +31,12 @@ const appStore = { giphy: createCachedImport(() => import("./giphy")), zapier: createCachedImport(() => import("./zapier")), make: createCachedImport(() => import("./make")), - exchange2013calendar: createCachedImport(() => import("./exchange2013calendar")), - exchange2016calendar: createCachedImport(() => import("./exchange2016calendar")), - exchangecalendar: createCachedImport(() => import("./exchangecalendar")), + // exchange2013calendar: createCachedImport(() => import("./exchange2013calendar")), + // exchange2016calendar: createCachedImport(() => import("./exchange2016calendar")), + // exchangecalendar: createCachedImport(() => import("./exchangecalendar")), facetime: createCachedImport(() => import("./facetime")), sylapsvideo: createCachedImport(() => import("./sylapsvideo")), - zohocalendar: createCachedImport(() => import("./zohocalendar")), + // zohocalendar: createCachedImport(() => import("./zohocalendar")), "zoho-bigin": createCachedImport(() => import("./zoho-bigin")), basecamp3: createCachedImport(() => import("./basecamp3")), telegramvideo: createCachedImport(() => import("./telegram")), From ba5d8dbc41e809a500b4f4ddfc0fa88f808297c4 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Wed, 20 Aug 2025 17:45:54 +0800 Subject: [PATCH 03/22] feat: add payment loaders. --- .../_utils/payments/paymentLoaders.ts | 18 ++++++++++++++++++ packages/app-store/index.ts | 16 ++++++++-------- packages/lib/getConnectedApps.ts | 6 ++++-- packages/lib/payment/deletePayment.ts | 8 +++++--- packages/lib/payment/handlePayment.ts | 6 +++--- packages/lib/payment/handlePaymentRefund.ts | 8 +++++--- .../trpc/server/routers/viewer/payments.tsx | 8 +++++--- .../viewer/payments/chargeCard.handler.ts | 8 +++++--- 8 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 packages/app-store/_utils/payments/paymentLoaders.ts diff --git a/packages/app-store/_utils/payments/paymentLoaders.ts b/packages/app-store/_utils/payments/paymentLoaders.ts new file mode 100644 index 00000000000000..e8b97503fda0dd --- /dev/null +++ b/packages/app-store/_utils/payments/paymentLoaders.ts @@ -0,0 +1,18 @@ +import { createCachedImport } from "../createCachedImport"; + +export const paymentLoaders = { + alby: createCachedImport(() => import("../../alby")), + paypal: createCachedImport(() => import("../../paypal")), + stripepayment: createCachedImport(() => import("../../stripepayment")), + hitpay: createCachedImport(() => import("../../hitpay")), + btcpayserver: createCachedImport(() => import("../../btcpayserver")), +}; + +if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { + // @ts-expect-error FIXME + paymentLoaders["mock-payment-app"] = createCachedImport(() => import("../../mock-payment-app/index")); +} + +export type PaymentLoaderKey = keyof typeof paymentLoaders & { + ["mock-payment-app"]?: () => Promise; +}; diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index dc4f8a34191d15..41d5568ae0aac4 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -1,5 +1,5 @@ const appStore = { - alby: createCachedImport(() => import("./alby")), + // alby: createCachedImport(() => import("./alby")), // applecalendar: createCachedImport(() => import("./applecalendar")), // caldavcalendar: createCachedImport(() => import("./caldavcalendar")), closecom: createCachedImport(() => import("./closecom")), @@ -17,12 +17,12 @@ const appStore = { // office365calendar: createCachedImport(() => import("./office365calendar")), office365video: createCachedImport(() => import("./office365video")), // plausible: createCachedImport(() => import("./plausible")), - paypal: createCachedImport(() => import("./paypal")), + // paypal: createCachedImport(() => import("./paypal")), "pipedrive-crm": createCachedImport(() => import("./pipedrive-crm")), salesforce: createCachedImport(() => import("./salesforce")), zohocrm: createCachedImport(() => import("./zohocrm")), sendgrid: createCachedImport(() => import("./sendgrid")), - stripepayment: createCachedImport(() => import("./stripepayment")), + // stripepayment: createCachedImport(() => import("./stripepayment")), tandemvideo: createCachedImport(() => import("./tandemvideo")), vital: createCachedImport(() => import("./vital")), zoomvideo: createCachedImport(() => import("./zoomvideo")), @@ -41,8 +41,8 @@ const appStore = { basecamp3: createCachedImport(() => import("./basecamp3")), telegramvideo: createCachedImport(() => import("./telegram")), shimmervideo: createCachedImport(() => import("./shimmervideo")), - hitpay: createCachedImport(() => import("./hitpay")), - btcpayserver: createCachedImport(() => import("./btcpayserver")), + // hitpay: createCachedImport(() => import("./hitpay")), + // btcpayserver: createCachedImport(() => import("./btcpayserver")), }; function createCachedImport(importFunc: () => Promise): () => Promise { @@ -60,8 +60,8 @@ const exportedAppStore: typeof appStore & { ["mock-payment-app"]?: () => Promise; } = appStore; -if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { - exportedAppStore["mock-payment-app"] = createCachedImport(() => import("./mock-payment-app/index")); -} +// if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { +// exportedAppStore["mock-payment-app"] = createCachedImport(() => import("./mock-payment-app/index")); +// } export default exportedAppStore; diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index 09567d64283b7f..306418264b7acf 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -1,7 +1,8 @@ import type { Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; import type { TDependencyData } from "@calcom/app-store/_appRegistry"; +import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { CredentialOwner } from "@calcom/app-store/types"; import { getAppFromSlug } from "@calcom/app-store/utils"; import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; @@ -184,7 +185,8 @@ export async function getConnectedApps({ // undefined it means that app don't require app/setup/page let isSetupAlready = undefined; if (credential && app.categories.includes("payment")) { - const paymentApp = (await appStore[app.dirName as keyof typeof appStore]?.()) as PaymentApp | null; + // @ts-expect-error FIXME + const paymentApp = (await paymentLoaders[app.dirName as PaymentLoaderKey]?.()) as PaymentApp | null; if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { const PaymentService = paymentApp.lib.PaymentService; const paymentInstance = new PaymentService(credential); diff --git a/packages/lib/payment/deletePayment.ts b/packages/lib/payment/deletePayment.ts index 20411bdf79c37b..690fb6324a1860 100644 --- a/packages/lib/payment/deletePayment.ts +++ b/packages/lib/payment/deletePayment.ts @@ -1,6 +1,7 @@ import type { Payment, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { AppCategories } from "@calcom/prisma/enums"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -15,8 +16,9 @@ const deletePayment = async ( } | null; } ): Promise => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore + // @ts-expect-error FIXME + const paymentApp = (await paymentLoaders[ + paymentAppCredentials?.app?.dirName as PaymentLoaderKey ]?.()) as PaymentApp; if (!paymentApp?.lib?.PaymentService) { console.warn(`payment App service of type ${paymentApp} is not implemented`); diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 536ecf73cd3f57..8c9f8d7d5fcaa1 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -1,6 +1,6 @@ import type { AppCategories, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; import type { CompleteEventType } from "@calcom/prisma/zod"; import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils"; @@ -52,11 +52,11 @@ const handlePayment = async ({ }) => { if (isDryRun) return null; const key = paymentAppCredentials?.app?.dirName; - if (!isKeyOf(appStore, key)) { + if (!isKeyOf(paymentLoaders, key)) { console.warn(`key: ${key} is not a valid key in appStore`); return null; } - const paymentApp = await appStore[key]?.(); + const paymentApp = await paymentLoaders[key]?.(); if (!isPaymentApp(paymentApp)) { console.warn(`payment App service of type ${paymentApp} is not implemented`); return null; diff --git a/packages/lib/payment/handlePaymentRefund.ts b/packages/lib/payment/handlePaymentRefund.ts index a51a12423ed15f..ca65346b461c5d 100644 --- a/packages/lib/payment/handlePaymentRefund.ts +++ b/packages/lib/payment/handlePaymentRefund.ts @@ -1,6 +1,7 @@ import type { Payment, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { AppCategories } from "@calcom/prisma/enums"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -15,8 +16,9 @@ const handlePaymentRefund = async ( } | null; } ) => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore + // @ts-expect-error FIXME + const paymentApp = (await paymentLoaders[ + paymentAppCredentials?.app?.dirName as PaymentLoaderKey ]?.()) as PaymentApp; if (!paymentApp?.lib?.PaymentService) { console.warn(`payment App service of type ${paymentApp} is not implemented`); diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index 1485cbc6684543..ca2f431c968cb6 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -1,6 +1,7 @@ import { z } from "zod"; -import appStore from "@calcom/app-store"; +import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails"; import { WebhookService } from "@calcom/features/webhooks/lib/WebhookService"; @@ -108,8 +109,9 @@ export const paymentsRouter = router({ throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid payment credential" }); } - const paymentApp = (await appStore[ - paymentCredential?.app?.dirName as keyof typeof appStore + // @ts-expect-error FIXME + const paymentApp = (await paymentLoaders[ + paymentCredential?.app?.dirName as PaymentLoaderKey ]?.()) as PaymentApp | null; if (!(paymentApp && paymentApp.lib && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) { diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index 44c577fa73feea..1c1d04997a0d51 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -1,4 +1,5 @@ -import appStore from "@calcom/app-store"; +import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails"; import { ErrorCode } from "@calcom/lib/errorCodes"; @@ -125,8 +126,9 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid payment credential" }); } - const paymentApp = (await appStore[ - paymentCredential?.app?.dirName as keyof typeof appStore + // @ts-expect-error FIXME + const paymentApp = (await paymentLoaders[ + paymentCredential?.app?.dirName as PaymentLoaderKey ]?.()) as PaymentApp; if (!paymentApp?.lib?.PaymentService) { From ca94ac04ee122b6aeb14bd34d70fe3d99486761c Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Wed, 20 Aug 2025 17:57:57 +0800 Subject: [PATCH 04/22] feat: add video loaders. --- .../app-store/_utils/videos/videoLoaders.ts | 17 ++++++++++++++ packages/app-store/index.ts | 22 +++++++++---------- packages/lib/videoClient.ts | 5 +++-- 3 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 packages/app-store/_utils/videos/videoLoaders.ts diff --git a/packages/app-store/_utils/videos/videoLoaders.ts b/packages/app-store/_utils/videos/videoLoaders.ts new file mode 100644 index 00000000000000..5bb61ae51d905a --- /dev/null +++ b/packages/app-store/_utils/videos/videoLoaders.ts @@ -0,0 +1,17 @@ +import { createCachedImport } from "../createCachedImport"; + +export const videoLoaders = { + dailyvideo: createCachedImport(() => import("../../dailyvideo")), + googlevideo: createCachedImport(() => import("../../googlevideo")), + huddle01video: createCachedImport(() => import("../../huddle01video")), + jellyconferencing: createCachedImport(() => import("../../jelly")), + jitsivideo: createCachedImport(() => import("../../jitsivideo")), + office365video: createCachedImport(() => import("../../office365video")), + tandemvideo: createCachedImport(() => import("../../tandemvideo")), + zoomvideo: createCachedImport(() => import("../../zoomvideo")), + webexvideo: createCachedImport(() => import("../../webex")), + sylapsvideo: createCachedImport(() => import("../../sylapsvideo")), + shimmervideo: createCachedImport(() => import("../../shimmervideo")), +}; + +export type VideoLoaderKey = keyof typeof videoLoaders; diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 41d5568ae0aac4..461cea9b5ef761 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -3,19 +3,19 @@ const appStore = { // applecalendar: createCachedImport(() => import("./applecalendar")), // caldavcalendar: createCachedImport(() => import("./caldavcalendar")), closecom: createCachedImport(() => import("./closecom")), - dailyvideo: createCachedImport(() => import("./dailyvideo")), + // dailyvideo: createCachedImport(() => import("./dailyvideo")), // dub: createCachedImport(() => import("./dub")), // googlecalendar: createCachedImport(() => import("./googlecalendar")), - googlevideo: createCachedImport(() => import("./googlevideo")), + // googlevideo: createCachedImport(() => import("./googlevideo")), hubspot: createCachedImport(() => import("./hubspot")), - huddle01video: createCachedImport(() => import("./huddle01video")), + // huddle01video: createCachedImport(() => import("./huddle01video")), // "ics-feedcalendar": createCachedImport(() => import("./ics-feedcalendar")), - jellyconferencing: createCachedImport(() => import("./jelly")), - jitsivideo: createCachedImport(() => import("./jitsivideo")), + // jellyconferencing: createCachedImport(() => import("./jelly")), + // jitsivideo: createCachedImport(() => import("./jitsivideo")), // larkcalendar: createCachedImport(() => import("./larkcalendar")), nextcloudtalkvideo: createCachedImport(() => import("./nextcloudtalk")), // office365calendar: createCachedImport(() => import("./office365calendar")), - office365video: createCachedImport(() => import("./office365video")), + // office365video: createCachedImport(() => import("./office365video")), // plausible: createCachedImport(() => import("./plausible")), // paypal: createCachedImport(() => import("./paypal")), "pipedrive-crm": createCachedImport(() => import("./pipedrive-crm")), @@ -23,11 +23,11 @@ const appStore = { zohocrm: createCachedImport(() => import("./zohocrm")), sendgrid: createCachedImport(() => import("./sendgrid")), // stripepayment: createCachedImport(() => import("./stripepayment")), - tandemvideo: createCachedImport(() => import("./tandemvideo")), + // tandemvideo: createCachedImport(() => import("./tandemvideo")), vital: createCachedImport(() => import("./vital")), - zoomvideo: createCachedImport(() => import("./zoomvideo")), + // zoomvideo: createCachedImport(() => import("./zoomvideo")), wipemycalother: createCachedImport(() => import("./wipemycalother")), - webexvideo: createCachedImport(() => import("./webex")), + // webexvideo: createCachedImport(() => import("./webex")), giphy: createCachedImport(() => import("./giphy")), zapier: createCachedImport(() => import("./zapier")), make: createCachedImport(() => import("./make")), @@ -35,12 +35,12 @@ const appStore = { // exchange2016calendar: createCachedImport(() => import("./exchange2016calendar")), // exchangecalendar: createCachedImport(() => import("./exchangecalendar")), facetime: createCachedImport(() => import("./facetime")), - sylapsvideo: createCachedImport(() => import("./sylapsvideo")), + // sylapsvideo: createCachedImport(() => import("./sylapsvideo")), // zohocalendar: createCachedImport(() => import("./zohocalendar")), "zoho-bigin": createCachedImport(() => import("./zoho-bigin")), basecamp3: createCachedImport(() => import("./basecamp3")), telegramvideo: createCachedImport(() => import("./telegram")), - shimmervideo: createCachedImport(() => import("./shimmervideo")), + // shimmervideo: createCachedImport(() => import("./shimmervideo")), // hitpay: createCachedImport(() => import("./hitpay")), // btcpayserver: createCachedImport(() => import("./btcpayserver")), }; diff --git a/packages/lib/videoClient.ts b/packages/lib/videoClient.ts index 1b4c51b505cc90..b8ba35422fe908 100644 --- a/packages/lib/videoClient.ts +++ b/packages/lib/videoClient.ts @@ -1,7 +1,8 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; -import appStore from "@calcom/app-store"; +import { videoLoaders } from "@calcom/app-store/_utils/videos/videoLoaders"; +import type { VideoLoaderKey } from "@calcom/app-store/_utils/videos/videoLoaders"; import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys"; import { DailyLocationType } from "@calcom/app-store/locations"; import { sendBrokenIntegrationEmail } from "@calcom/emails"; @@ -27,7 +28,7 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise Date: Wed, 20 Aug 2025 17:59:55 +0800 Subject: [PATCH 05/22] chore: remove commented code. --- packages/app-store/index.ts | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 461cea9b5ef761..3836b4062b83ec 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -1,48 +1,20 @@ const appStore = { - // alby: createCachedImport(() => import("./alby")), - // applecalendar: createCachedImport(() => import("./applecalendar")), - // caldavcalendar: createCachedImport(() => import("./caldavcalendar")), closecom: createCachedImport(() => import("./closecom")), - // dailyvideo: createCachedImport(() => import("./dailyvideo")), - // dub: createCachedImport(() => import("./dub")), - // googlecalendar: createCachedImport(() => import("./googlecalendar")), - // googlevideo: createCachedImport(() => import("./googlevideo")), hubspot: createCachedImport(() => import("./hubspot")), - // huddle01video: createCachedImport(() => import("./huddle01video")), - // "ics-feedcalendar": createCachedImport(() => import("./ics-feedcalendar")), - // jellyconferencing: createCachedImport(() => import("./jelly")), - // jitsivideo: createCachedImport(() => import("./jitsivideo")), - // larkcalendar: createCachedImport(() => import("./larkcalendar")), nextcloudtalkvideo: createCachedImport(() => import("./nextcloudtalk")), - // office365calendar: createCachedImport(() => import("./office365calendar")), - // office365video: createCachedImport(() => import("./office365video")), - // plausible: createCachedImport(() => import("./plausible")), - // paypal: createCachedImport(() => import("./paypal")), "pipedrive-crm": createCachedImport(() => import("./pipedrive-crm")), salesforce: createCachedImport(() => import("./salesforce")), zohocrm: createCachedImport(() => import("./zohocrm")), sendgrid: createCachedImport(() => import("./sendgrid")), - // stripepayment: createCachedImport(() => import("./stripepayment")), - // tandemvideo: createCachedImport(() => import("./tandemvideo")), vital: createCachedImport(() => import("./vital")), - // zoomvideo: createCachedImport(() => import("./zoomvideo")), wipemycalother: createCachedImport(() => import("./wipemycalother")), - // webexvideo: createCachedImport(() => import("./webex")), giphy: createCachedImport(() => import("./giphy")), zapier: createCachedImport(() => import("./zapier")), make: createCachedImport(() => import("./make")), - // exchange2013calendar: createCachedImport(() => import("./exchange2013calendar")), - // exchange2016calendar: createCachedImport(() => import("./exchange2016calendar")), - // exchangecalendar: createCachedImport(() => import("./exchangecalendar")), facetime: createCachedImport(() => import("./facetime")), - // sylapsvideo: createCachedImport(() => import("./sylapsvideo")), - // zohocalendar: createCachedImport(() => import("./zohocalendar")), "zoho-bigin": createCachedImport(() => import("./zoho-bigin")), basecamp3: createCachedImport(() => import("./basecamp3")), telegramvideo: createCachedImport(() => import("./telegram")), - // shimmervideo: createCachedImport(() => import("./shimmervideo")), - // hitpay: createCachedImport(() => import("./hitpay")), - // btcpayserver: createCachedImport(() => import("./btcpayserver")), }; function createCachedImport(importFunc: () => Promise): () => Promise { @@ -56,12 +28,6 @@ function createCachedImport(importFunc: () => Promise): () => Promise { }; } -const exportedAppStore: typeof appStore & { - ["mock-payment-app"]?: () => Promise; -} = appStore; - -// if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { -// exportedAppStore["mock-payment-app"] = createCachedImport(() => import("./mock-payment-app/index")); -// } +const exportedAppStore: typeof appStore = appStore; export default exportedAppStore; From f20b1cb4730ae7cb0ac0b5f356201ece5e109d47 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Wed, 20 Aug 2025 18:09:28 +0800 Subject: [PATCH 06/22] chore: fix payment loader key type. --- packages/app-store/_utils/payments/paymentLoaders.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app-store/_utils/payments/paymentLoaders.ts b/packages/app-store/_utils/payments/paymentLoaders.ts index e8b97503fda0dd..4d08506804d00d 100644 --- a/packages/app-store/_utils/payments/paymentLoaders.ts +++ b/packages/app-store/_utils/payments/paymentLoaders.ts @@ -13,6 +13,4 @@ if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { paymentLoaders["mock-payment-app"] = createCachedImport(() => import("../../mock-payment-app/index")); } -export type PaymentLoaderKey = keyof typeof paymentLoaders & { - ["mock-payment-app"]?: () => Promise; -}; +export type PaymentLoaderKey = keyof typeof paymentLoaders | "mock-payment-app"; From c41afcde0153101b780696e49295b5d0a0781f70 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Wed, 20 Aug 2025 20:30:01 +0800 Subject: [PATCH 07/22] test: revert app store data change for type check --- packages/app-store/index.ts | 36 ++++++++++++++++++++++++++++++- tests/libs/__mocks__/app-store.ts | 1 + 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 3836b4062b83ec..8ddea439c5fe0d 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -1,20 +1,48 @@ const appStore = { + alby: createCachedImport(() => import("./alby")), + applecalendar: createCachedImport(() => import("./applecalendar")), + caldavcalendar: createCachedImport(() => import("./caldavcalendar")), closecom: createCachedImport(() => import("./closecom")), + dailyvideo: createCachedImport(() => import("./dailyvideo")), + dub: createCachedImport(() => import("./dub")), + googlecalendar: createCachedImport(() => import("./googlecalendar")), + googlevideo: createCachedImport(() => import("./googlevideo")), hubspot: createCachedImport(() => import("./hubspot")), + huddle01video: createCachedImport(() => import("./huddle01video")), + "ics-feedcalendar": createCachedImport(() => import("./ics-feedcalendar")), + jellyconferencing: createCachedImport(() => import("./jelly")), + jitsivideo: createCachedImport(() => import("./jitsivideo")), + larkcalendar: createCachedImport(() => import("./larkcalendar")), nextcloudtalkvideo: createCachedImport(() => import("./nextcloudtalk")), + office365calendar: createCachedImport(() => import("./office365calendar")), + office365video: createCachedImport(() => import("./office365video")), + plausible: createCachedImport(() => import("./plausible")), + paypal: createCachedImport(() => import("./paypal")), "pipedrive-crm": createCachedImport(() => import("./pipedrive-crm")), salesforce: createCachedImport(() => import("./salesforce")), zohocrm: createCachedImport(() => import("./zohocrm")), sendgrid: createCachedImport(() => import("./sendgrid")), + stripepayment: createCachedImport(() => import("./stripepayment")), + tandemvideo: createCachedImport(() => import("./tandemvideo")), vital: createCachedImport(() => import("./vital")), + zoomvideo: createCachedImport(() => import("./zoomvideo")), wipemycalother: createCachedImport(() => import("./wipemycalother")), + webexvideo: createCachedImport(() => import("./webex")), giphy: createCachedImport(() => import("./giphy")), zapier: createCachedImport(() => import("./zapier")), make: createCachedImport(() => import("./make")), + exchange2013calendar: createCachedImport(() => import("./exchange2013calendar")), + exchange2016calendar: createCachedImport(() => import("./exchange2016calendar")), + exchangecalendar: createCachedImport(() => import("./exchangecalendar")), facetime: createCachedImport(() => import("./facetime")), + sylapsvideo: createCachedImport(() => import("./sylapsvideo")), + zohocalendar: createCachedImport(() => import("./zohocalendar")), "zoho-bigin": createCachedImport(() => import("./zoho-bigin")), basecamp3: createCachedImport(() => import("./basecamp3")), telegramvideo: createCachedImport(() => import("./telegram")), + shimmervideo: createCachedImport(() => import("./shimmervideo")), + hitpay: createCachedImport(() => import("./hitpay")), + btcpayserver: createCachedImport(() => import("./btcpayserver")), }; function createCachedImport(importFunc: () => Promise): () => Promise { @@ -28,6 +56,12 @@ function createCachedImport(importFunc: () => Promise): () => Promise { }; } -const exportedAppStore: typeof appStore = appStore; +const exportedAppStore: typeof appStore & { + ["mock-payment-app"]?: () => Promise; +} = appStore; + +if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { + exportedAppStore["mock-payment-app"] = createCachedImport(() => import("./mock-payment-app/index")); +} export default exportedAppStore; diff --git a/tests/libs/__mocks__/app-store.ts b/tests/libs/__mocks__/app-store.ts index 01da9339d5a88a..bf4462b133b393 100644 --- a/tests/libs/__mocks__/app-store.ts +++ b/tests/libs/__mocks__/app-store.ts @@ -1,6 +1,7 @@ import { beforeEach, vi } from "vitest"; import { mockReset, mockDeep } from "vitest-mock-extended"; +// FIXME is it okay to use the large type here? import type * as appStore from "@calcom/app-store"; vi.mock("@calcom/app-store", () => appStoreMock); From 310ce652f8fd1d5a59ae09de8eb679f3a1a585e7 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Wed, 20 Aug 2025 21:37:19 +0800 Subject: [PATCH 08/22] feat: add `getCalendarApps` util fn. --- packages/app-store-cli/src/build.ts | 26 ++++ .../_utils/calendars/getCalendarApps.ts | 111 ++++++++++++++++++ .../calendarApps.metadata.generated.ts | 35 ++++++ packages/lib/CalendarManager.ts | 4 +- 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 packages/app-store/_utils/calendars/getCalendarApps.ts create mode 100644 packages/app-store/calendarApps.metadata.generated.ts diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 1d9c5d208cc38d..258dd1269708c8 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -38,6 +38,7 @@ function generateFiles() { const appKeysSchemasOutput = []; const serverOutput = []; const crmOutput = []; + const calendarMetadataOutput = []; const appDirs: { name: string; path: string }[] = []; fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) { @@ -345,6 +346,26 @@ function generateFiles() { ) ); + calendarMetadataOutput.push( + ...getExportedObject( + "calendarAppsMetadata", + { + // Try looking for config.json and if it's not found use _metadata.ts to generate appStoreMetadata + importConfig: [ + { + fileToBeImported: "config.json", + importName: "default", + }, + { + fileToBeImported: "_metadata.ts", + importName: "metadata", + }, + ], + }, + isCalendarApp + ) + ); + const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. Don't modify this file manually. @@ -358,6 +379,7 @@ function generateFiles() { ["apps.keys-schemas.generated.ts", appKeysSchemasOutput], ["bookerApps.metadata.generated.ts", bookerMetadataOutput], ["crm.apps.generated.ts", crmOutput], + ["calendarApps.metadata.generated.ts", calendarMetadataOutput], ]; filesToGenerate.forEach(([fileName, output]) => { fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); @@ -405,3 +427,7 @@ function isBookerApp(app: App) { function isCrmApp(app: App) { return !!app.categories?.includes("crm"); } + +function isCalendarApp(app: App) { + return !!app.categories?.includes("calendar"); +} diff --git a/packages/app-store/_utils/calendars/getCalendarApps.ts b/packages/app-store/_utils/calendars/getCalendarApps.ts new file mode 100644 index 00000000000000..58324c3b01e3a7 --- /dev/null +++ b/packages/app-store/_utils/calendars/getCalendarApps.ts @@ -0,0 +1,111 @@ +import logger from "@calcom/lib/logger"; +import { getPiiFreeCredential } from "@calcom/lib/piiFreeData"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import type { App } from "@calcom/types/App"; +import type { AppMeta } from "@calcom/types/App"; +import type { CredentialForCalendarService } from "@calcom/types/Credential"; + +import { calendarAppsMetadata as rawCalendarAppsMetadata } from "../../calendarApps.metadata.generated"; +import { getNormalizedAppMetadata } from "../../getNormalizedAppMetadata"; +import type { EventLocationType } from "../../locations"; + +export type LocationOption = { + label: string; + value: EventLocationType["type"]; + icon?: string; + disabled?: boolean; +}; + +export type CredentialDataWithTeamName = CredentialForCalendarService & { + team?: { + name: string; + } | null; +}; + +type RawCalendarAppsMetaData = typeof rawCalendarAppsMetadata; +type CalendarAppsMetaData = { + [key in keyof RawCalendarAppsMetaData]: Omit & { dirName: string }; +}; + +export const calendarAppsMetadata = {} as CalendarAppsMetaData; +for (const [key, value] of Object.entries(rawCalendarAppsMetadata)) { + calendarAppsMetadata[key as keyof typeof calendarAppsMetadata] = getNormalizedAppMetadata(value); +} + +const CALENDAR_APPS_MAP = Object.keys(calendarAppsMetadata).reduce((store, key) => { + const metadata = calendarAppsMetadata[key as keyof typeof calendarAppsMetadata] as AppMeta; + + store[key] = metadata; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete store[key]["/*"]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete store[key]["__createdUsingCli"]; + return store; +}, {} as Record); + +const ALL_CALENDAR_APPS = Object.values(CALENDAR_APPS_MAP); + +export const getCalendarApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => { + const apps = ALL_CALENDAR_APPS.reduce((reducedArray, appMeta) => { + const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug); + + if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; + + let locationOption: LocationOption | null = null; + + /** If the app is a globally installed one, let's inject it's key */ + if (appMeta.isGlobal) { + const credential = { + id: 0, + type: appMeta.type, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + key: appMeta.key!, + userId: 0, + user: { email: "" }, + teamId: null, + appId: appMeta.slug, + invalid: false, + delegatedTo: null, + delegatedToId: null, + delegationCredentialId: null, + team: { + name: "Global", + }, + }; + logger.debug( + `${appMeta.type} is a global app, injecting credential`, + safeStringify(getPiiFreeCredential(credential)) + ); + appCredentials.push(credential); + } + + /** Check if app has location option AND add it if user has credentials for it */ + if (appCredentials.length > 0 && appMeta?.appData?.location) { + locationOption = { + value: appMeta.appData.location.type, + label: appMeta.appData.location.label || "No label set", + disabled: false, + }; + } + + const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; + + reducedArray.push({ + ...appMeta, + /** + * @deprecated use `credentials` + */ + credential, + credentials: appCredentials, + /** Option to display in `location` field while editing event types */ + locationOption, + }); + + return reducedArray; + }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); + + return apps; +}; diff --git a/packages/app-store/calendarApps.metadata.generated.ts b/packages/app-store/calendarApps.metadata.generated.ts new file mode 100644 index 00000000000000..f2efca222f5483 --- /dev/null +++ b/packages/app-store/calendarApps.metadata.generated.ts @@ -0,0 +1,35 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +import amie_config_json from "./amie/config.json"; +import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata"; +import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata"; +import cron_config_json from "./cron/config.json"; +import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata"; +import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata"; +import exchangecalendar_config_json from "./exchangecalendar/config.json"; +import { metadata as feishucalendar__metadata_ts } from "./feishucalendar/_metadata"; +import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata"; +import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json"; +import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata"; +import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata"; +import vimcal_config_json from "./vimcal/config.json"; +import zohocalendar_config_json from "./zohocalendar/config.json"; + +export const calendarAppsMetadata = { + amie: amie_config_json, + applecalendar: applecalendar__metadata_ts, + caldavcalendar: caldavcalendar__metadata_ts, + cron: cron_config_json, + exchange2013calendar: exchange2013calendar__metadata_ts, + exchange2016calendar: exchange2016calendar__metadata_ts, + exchangecalendar: exchangecalendar_config_json, + feishucalendar: feishucalendar__metadata_ts, + googlecalendar: googlecalendar__metadata_ts, + "ics-feedcalendar": ics_feedcalendar_config_json, + larkcalendar: larkcalendar__metadata_ts, + office365calendar: office365calendar__metadata_ts, + vimcal: vimcal_config_json, + zohocalendar: zohocalendar_config_json, +}; diff --git a/packages/lib/CalendarManager.ts b/packages/lib/CalendarManager.ts index 8d29874a3a4555..2c9e0c7ba62b67 100644 --- a/packages/lib/CalendarManager.ts +++ b/packages/lib/CalendarManager.ts @@ -1,8 +1,8 @@ // eslint-disable-next-line no-restricted-imports import { sortBy } from "lodash"; +import { getCalendarApps } from "@calcom/app-store/_utils/calendars/getCalendarApps"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; -import getApps from "@calcom/app-store/utils"; import dayjs from "@calcom/dayjs"; import { getUid } from "@calcom/lib/CalEventParser"; import { getRichDescription } from "@calcom/lib/CalEventParser"; @@ -30,7 +30,7 @@ import { getCalendarsEventsWithTimezones } from "./getCalendarsEvents"; const log = logger.getSubLogger({ prefix: ["CalendarManager"] }); export const getCalendarCredentials = (credentials: Array) => { - const calendarCredentials = getApps(credentials, true) + const calendarCredentials = getCalendarApps(credentials, true) .filter((app) => app.type.endsWith("_calendar")) .flatMap((app) => { const credentials = app.credentials.flatMap((credential) => { From 24706da20d3f3a7cb3512d24ae43c1d1bd5d0ceb Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Thu, 21 Aug 2025 14:21:05 +0800 Subject: [PATCH 09/22] refactor: add calendar apps metadata handling into separate file --- .../_utils/calendars/calendarAppsMetaData.ts | 14 ++++++++++++++ .../_utils/calendars/getCalendarApps.ts | 17 +++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 packages/app-store/_utils/calendars/calendarAppsMetaData.ts diff --git a/packages/app-store/_utils/calendars/calendarAppsMetaData.ts b/packages/app-store/_utils/calendars/calendarAppsMetaData.ts new file mode 100644 index 00000000000000..9ba751de35de14 --- /dev/null +++ b/packages/app-store/_utils/calendars/calendarAppsMetaData.ts @@ -0,0 +1,14 @@ +import type { AppMeta } from "@calcom/types/App"; + +import { calendarAppsMetadata as rawCalendarAppsMetadata } from "../../calendarApps.metadata.generated"; +import { getNormalizedAppMetadata } from "../../getNormalizedAppMetadata"; + +type RawCalendarAppsMetaData = typeof rawCalendarAppsMetadata; +type CalendarAppsMetaData = { + [key in keyof RawCalendarAppsMetaData]: Omit & { dirName: string }; +}; + +export const calendarAppsMetaData = {} as CalendarAppsMetaData; +for (const [key, value] of Object.entries(rawCalendarAppsMetadata)) { + calendarAppsMetaData[key as keyof typeof calendarAppsMetaData] = getNormalizedAppMetadata(value); +} diff --git a/packages/app-store/_utils/calendars/getCalendarApps.ts b/packages/app-store/_utils/calendars/getCalendarApps.ts index 58324c3b01e3a7..8a3f220bbbf7f3 100644 --- a/packages/app-store/_utils/calendars/getCalendarApps.ts +++ b/packages/app-store/_utils/calendars/getCalendarApps.ts @@ -5,9 +5,8 @@ import type { App } from "@calcom/types/App"; import type { AppMeta } from "@calcom/types/App"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; -import { calendarAppsMetadata as rawCalendarAppsMetadata } from "../../calendarApps.metadata.generated"; -import { getNormalizedAppMetadata } from "../../getNormalizedAppMetadata"; import type { EventLocationType } from "../../locations"; +import { calendarAppsMetaData } from "./calendarAppsMetaData"; export type LocationOption = { label: string; @@ -22,18 +21,8 @@ export type CredentialDataWithTeamName = CredentialForCalendarService & { } | null; }; -type RawCalendarAppsMetaData = typeof rawCalendarAppsMetadata; -type CalendarAppsMetaData = { - [key in keyof RawCalendarAppsMetaData]: Omit & { dirName: string }; -}; - -export const calendarAppsMetadata = {} as CalendarAppsMetaData; -for (const [key, value] of Object.entries(rawCalendarAppsMetadata)) { - calendarAppsMetadata[key as keyof typeof calendarAppsMetadata] = getNormalizedAppMetadata(value); -} - -const CALENDAR_APPS_MAP = Object.keys(calendarAppsMetadata).reduce((store, key) => { - const metadata = calendarAppsMetadata[key as keyof typeof calendarAppsMetadata] as AppMeta; +const CALENDAR_APPS_MAP = Object.keys(calendarAppsMetaData).reduce((store, key) => { + const metadata = calendarAppsMetaData[key as keyof typeof calendarAppsMetaData] as AppMeta; store[key] = metadata; From f04a8fa365e3bb474a58504ce508262c8f27932c Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Thu, 21 Aug 2025 15:32:02 +0800 Subject: [PATCH 10/22] test: add calendar and video apps mocks --- .../utils/bookingScenario/bookingScenario.ts | 23 +++++++++++-------- tests/libs/__mocks__/calendarApps.ts | 18 +++++++++++++++ tests/libs/__mocks__/videoApps.ts | 18 +++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 tests/libs/__mocks__/calendarApps.ts create mode 100644 tests/libs/__mocks__/videoApps.ts diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 0869ef6c003699..e68b1fdf39522d 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -1,6 +1,8 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; +import { calendarAppsMock } from "../../../../../tests/libs/__mocks__/calendarApps"; import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; import prismock from "../../../../../tests/libs/__mocks__/prisma"; +import { videoAppsMock } from "../../../../../tests/libs/__mocks__/videoApps"; import type { BookingReference, Attendee, Booking, Membership } from "@prisma/client"; import type { Prisma } from "@prisma/client"; @@ -11,6 +13,7 @@ import { vi } from "vitest"; import "vitest-fetch-mock"; import type { z } from "zod"; +import { calendarAppsMetaData } from "@calcom/app-store/_utils/calendars/calendarAppsMetaData"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook"; import { weekdayToWeekIndex, type WeekDays } from "@calcom/lib/dayjs"; @@ -1716,7 +1719,7 @@ export type CalendarServiceMethodMock = { * @param calendarData Specify uids and other data to be faked to be returned by createEvent and updateEvent */ export function mockCalendar( - metadataLookupKey: keyof typeof appStoreMetadata, + metadataLookupKey: keyof typeof calendarAppsMetaData, calendarData?: { create?: { id?: string; @@ -1759,15 +1762,15 @@ export function mockCalendar( uid: "UPDATED_MOCK_ID", }, }; - log.silly(`Mocking ${appStoreLookupKey} on appStoreMock`); + log.silly(`Mocking ${appStoreLookupKey} on calendarAppsMock`); const createEventCalls: CreateEventMethodMockCall[] = []; const updateEventCalls: UpdateEventMethodMockCall[] = []; const deleteEventCalls: DeleteEventMethodMockCall[] = []; const getAvailabilityCalls: GetAvailabilityMethodMockCall[] = []; - const app = appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata]; + const app = calendarAppsMetaData[metadataLookupKey as keyof typeof calendarAppsMetaData]; - const appMock = appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default]; + const appMock = calendarAppsMock[appStoreLookupKey as keyof typeof calendarAppsMock]; appMock && `mockResolvedValue` in appMock && @@ -1899,7 +1902,7 @@ export function mockCalendar( } export function mockCalendarToHaveNoBusySlots( - metadataLookupKey: keyof typeof appStoreMetadata, + metadataLookupKey: keyof typeof calendarAppsMetaData, calendarData?: Parameters[1] ) { calendarData = calendarData || { @@ -1913,11 +1916,11 @@ export function mockCalendarToHaveNoBusySlots( return mockCalendar(metadataLookupKey, { ...calendarData, busySlots: [] }); } -export function mockCalendarToCrashOnCreateEvent(metadataLookupKey: keyof typeof appStoreMetadata) { +export function mockCalendarToCrashOnCreateEvent(metadataLookupKey: keyof typeof calendarAppsMetaData) { return mockCalendar(metadataLookupKey, { creationCrash: true }); } -export function mockCalendarToCrashOnUpdateEvent(metadataLookupKey: keyof typeof appStoreMetadata) { +export function mockCalendarToCrashOnUpdateEvent(metadataLookupKey: keyof typeof calendarAppsMetaData) { return mockCalendar(metadataLookupKey, { updationCrash: true }); } @@ -1952,13 +1955,13 @@ export function mockVideoApp({ // eslint-disable-next-line @typescript-eslint/no-explicit-any const deleteMeetingCalls: any[] = []; // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => { + // @ts-expect-error FIXME + videoAppsMock[appStoreLookupKey as keyof typeof videoAppsMock].mockImplementation(() => { return new Promise((resolve) => { resolve({ lib: { // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore + // @ts-expect-error FIXME VideoApiAdapter: (credential) => { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/tests/libs/__mocks__/calendarApps.ts b/tests/libs/__mocks__/calendarApps.ts new file mode 100644 index 00000000000000..d3a734619b92ab --- /dev/null +++ b/tests/libs/__mocks__/calendarApps.ts @@ -0,0 +1,18 @@ +import { beforeEach, vi } from "vitest"; +import { mockReset, mockDeep } from "vitest-mock-extended"; + +import type { calendarLoaders } from "@calcom/app-store/_utils/calendars/calendarLoaders"; + +vi.mock("@calcom/app-store/_utils/calendar/calendarLoaders", () => calendarAppsMock); + +beforeEach(() => { + mockReset(calendarAppsMock); +}); + +export const calendarAppsMock = mockDeep({ + fallbackMockImplementation: () => { + throw new Error( + "Unimplemented calendarAppsMock. You seem to have not mocked the app that you are trying to use" + ); + }, +}); diff --git a/tests/libs/__mocks__/videoApps.ts b/tests/libs/__mocks__/videoApps.ts new file mode 100644 index 00000000000000..86904f86a0dde5 --- /dev/null +++ b/tests/libs/__mocks__/videoApps.ts @@ -0,0 +1,18 @@ +import { beforeEach, vi } from "vitest"; +import { mockReset, mockDeep } from "vitest-mock-extended"; + +import type { videoLoaders } from "@calcom/app-store/_utils/videos/videoLoaders"; + +vi.mock("@calcom/app-store/_utils/videos/videoLoaders", () => videoAppsMock); + +beforeEach(() => { + mockReset(videoAppsMock); +}); + +export const videoAppsMock = mockDeep({ + fallbackMockImplementation: () => { + throw new Error( + "Unimplemented videoAppsMock. You seem to have not mocked the app that you are trying to use" + ); + }, +}); From 6b7f01c21247ddade786488894700941e71c3500 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Thu, 21 Aug 2025 15:56:40 +0800 Subject: [PATCH 11/22] test: add payment apps mock --- .../utils/bookingScenario/bookingScenario.ts | 9 +++++---- tests/libs/__mocks__/paymentApps.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 tests/libs/__mocks__/paymentApps.ts diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index e68b1fdf39522d..62bdc2cebf171b 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -1,6 +1,7 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; import { calendarAppsMock } from "../../../../../tests/libs/__mocks__/calendarApps"; import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; +import { paymentAppsMock } from "../../../../../tests/libs/__mocks__/paymentApps"; import prismock from "../../../../../tests/libs/__mocks__/prisma"; import { videoAppsMock } from "../../../../../tests/libs/__mocks__/videoApps"; @@ -2067,10 +2068,12 @@ export function mockPaymentApp({ const { paymentUid, externalId, MockPaymentService } = getMockPaymentService(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => { + paymentAppsMock[appStoreLookupKey as keyof typeof paymentAppsMock].mockImplementation(() => { return new Promise((resolve) => { resolve({ lib: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error FIXME PaymentService: MockPaymentService, }, }); @@ -2093,12 +2096,10 @@ export function mockErrorOnVideoMeetingCreation({ appStoreLookupKey = appStoreLookupKey || metadataLookupKey; // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - appStoreMock.default[appStoreLookupKey].mockImplementation(() => { + videoAppsMock[appStoreLookupKey].mockImplementation(() => { return new Promise((resolve) => { resolve({ lib: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore VideoApiAdapter: () => ({ createMeeting: () => { throw new MockError("Error creating Video meeting"); diff --git a/tests/libs/__mocks__/paymentApps.ts b/tests/libs/__mocks__/paymentApps.ts new file mode 100644 index 00000000000000..e0bf9986166361 --- /dev/null +++ b/tests/libs/__mocks__/paymentApps.ts @@ -0,0 +1,18 @@ +import { beforeEach, vi } from "vitest"; +import { mockReset, mockDeep } from "vitest-mock-extended"; + +import type { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; + +vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => paymentAppsMock); + +beforeEach(() => { + mockReset(paymentAppsMock); +}); + +export const paymentAppsMock = mockDeep({ + fallbackMockImplementation: () => { + throw new Error( + "Unimplemented paymentAppsMock. You seem to have not mocked the app that you are trying to use" + ); + }, +}); From 9445f491b9d811cfec03533bf7e69d7275df12c0 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Fri, 22 Aug 2025 11:02:50 +0800 Subject: [PATCH 12/22] test: correct import path for `calendarLoaders` mock --- tests/libs/__mocks__/calendarApps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/libs/__mocks__/calendarApps.ts b/tests/libs/__mocks__/calendarApps.ts index d3a734619b92ab..5df70cc61f5b6d 100644 --- a/tests/libs/__mocks__/calendarApps.ts +++ b/tests/libs/__mocks__/calendarApps.ts @@ -3,7 +3,7 @@ import { mockReset, mockDeep } from "vitest-mock-extended"; import type { calendarLoaders } from "@calcom/app-store/_utils/calendars/calendarLoaders"; -vi.mock("@calcom/app-store/_utils/calendar/calendarLoaders", () => calendarAppsMock); +vi.mock("@calcom/app-store/_utils/calendars/calendarLoaders", () => calendarAppsMock); beforeEach(() => { mockReset(calendarAppsMock); From 2819c0f2fdfe07b573bcd65d2eeda502bfdbcfc4 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Fri, 22 Aug 2025 16:19:14 +0800 Subject: [PATCH 13/22] feat: implement video apps handling and metadata generation test: mock default export instead of named export. --- .../services/conferencing.service.ts | 5 +- .../utils/bookingScenario/bookingScenario.ts | 10 +- packages/app-store-cli/src/build.ts | 26 +++++ .../_utils/calendars/calendarLoaders.ts | 4 +- packages/app-store/_utils/getCalendar.ts | 2 +- .../app-store/_utils/videos/getVideoApps.ts | 100 ++++++++++++++++++ .../_utils/videos/videoAppsMetaData.ts | 14 +++ .../app-store/_utils/videos/videoLoaders.ts | 4 +- .../app-store/videoApps.metadata.generated.ts | 65 ++++++++++++ packages/lib/EventManager.ts | 8 +- packages/lib/server/getDefaultLocations.ts | 4 +- packages/lib/videoClient.ts | 2 +- ...pdateUserDefaultConferencingApp.handler.ts | 4 +- tests/libs/__mocks__/calendarApps.ts | 6 +- tests/libs/__mocks__/videoApps.ts | 6 +- 15 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 packages/app-store/_utils/videos/getVideoApps.ts create mode 100644 packages/app-store/_utils/videos/videoAppsMetaData.ts create mode 100644 packages/app-store/videoApps.metadata.generated.ts diff --git a/apps/api/v2/src/modules/conferencing/services/conferencing.service.ts b/apps/api/v2/src/modules/conferencing/services/conferencing.service.ts index db2058f66ad407..77ada5ea64ca56 100644 --- a/apps/api/v2/src/modules/conferencing/services/conferencing.service.ts +++ b/apps/api/v2/src/modules/conferencing/services/conferencing.service.ts @@ -14,6 +14,7 @@ import { } from "@nestjs/common"; import { Injectable } from "@nestjs/common"; +import { getVideoApps } from "@calcom/app-store/_utils/videos/getVideoApps"; import { CONFERENCING_APPS, CAL_VIDEO, @@ -23,7 +24,7 @@ import { } from "@calcom/platform-constants"; import { userMetadata } from "@calcom/platform-libraries"; import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/platform-libraries/app-store"; -import { getApps, handleDeleteCredential } from "@calcom/platform-libraries/app-store"; +import { handleDeleteCredential } from "@calcom/platform-libraries/app-store"; @Injectable() export class ConferencingService { @@ -93,7 +94,7 @@ export class ConferencingService { } const credentials = await getUsersCredentialsIncludeServiceAccountKey(user); - const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0]; + const foundApp = getVideoApps(credentials, true).filter((app) => app.slug === appSlug)[0]; const appLocation = foundApp?.appData?.location; diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 62bdc2cebf171b..ca3831016e4c2e 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -1,9 +1,9 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; -import { calendarAppsMock } from "../../../../../tests/libs/__mocks__/calendarApps"; +import calendarAppsMock from "../../../../../tests/libs/__mocks__/calendarApps"; import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; import { paymentAppsMock } from "../../../../../tests/libs/__mocks__/paymentApps"; import prismock from "../../../../../tests/libs/__mocks__/prisma"; -import { videoAppsMock } from "../../../../../tests/libs/__mocks__/videoApps"; +import videoAppsMock from "../../../../../tests/libs/__mocks__/videoApps"; import type { BookingReference, Attendee, Booking, Membership } from "@prisma/client"; import type { Prisma } from "@prisma/client"; @@ -1771,7 +1771,7 @@ export function mockCalendar( const getAvailabilityCalls: GetAvailabilityMethodMockCall[] = []; const app = calendarAppsMetaData[metadataLookupKey as keyof typeof calendarAppsMetaData]; - const appMock = calendarAppsMock[appStoreLookupKey as keyof typeof calendarAppsMock]; + const appMock = calendarAppsMock.default[appStoreLookupKey as keyof typeof calendarAppsMock.default]; appMock && `mockResolvedValue` in appMock && @@ -1957,7 +1957,7 @@ export function mockVideoApp({ const deleteMeetingCalls: any[] = []; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error FIXME - videoAppsMock[appStoreLookupKey as keyof typeof videoAppsMock].mockImplementation(() => { + videoAppsMock.default[appStoreLookupKey as keyof typeof videoAppsMock.default].mockImplementation(() => { return new Promise((resolve) => { resolve({ lib: { @@ -2096,7 +2096,7 @@ export function mockErrorOnVideoMeetingCreation({ appStoreLookupKey = appStoreLookupKey || metadataLookupKey; // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - videoAppsMock[appStoreLookupKey].mockImplementation(() => { + videoAppsMock.default[appStoreLookupKey].mockImplementation(() => { return new Promise((resolve) => { resolve({ lib: { diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 258dd1269708c8..94b3e201cbe269 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -39,6 +39,7 @@ function generateFiles() { const serverOutput = []; const crmOutput = []; const calendarMetadataOutput = []; + const videoAppsMetadataOutput = []; const appDirs: { name: string; path: string }[] = []; fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) { @@ -366,6 +367,26 @@ function generateFiles() { ) ); + videoAppsMetadataOutput.push( + ...getExportedObject( + "videoAppsMetadata", + { + // Try looking for config.json and if it's not found use _metadata.ts to generate appStoreMetadata + importConfig: [ + { + fileToBeImported: "config.json", + importName: "default", + }, + { + fileToBeImported: "_metadata.ts", + importName: "metadata", + }, + ], + }, + isVideoApp + ) + ); + const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. Don't modify this file manually. @@ -380,6 +401,7 @@ function generateFiles() { ["bookerApps.metadata.generated.ts", bookerMetadataOutput], ["crm.apps.generated.ts", crmOutput], ["calendarApps.metadata.generated.ts", calendarMetadataOutput], + ["videoApps.metadata.generated.ts", videoAppsMetadataOutput], ]; filesToGenerate.forEach(([fileName, output]) => { fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); @@ -431,3 +453,7 @@ function isCrmApp(app: App) { function isCalendarApp(app: App) { return !!app.categories?.includes("calendar"); } + +function isVideoApp(app: App) { + return !!app.categories?.includes("video") || !!app.categories?.includes("conferencing"); +} diff --git a/packages/app-store/_utils/calendars/calendarLoaders.ts b/packages/app-store/_utils/calendars/calendarLoaders.ts index 9886defff7f91c..75aebc4899ead2 100644 --- a/packages/app-store/_utils/calendars/calendarLoaders.ts +++ b/packages/app-store/_utils/calendars/calendarLoaders.ts @@ -1,6 +1,6 @@ import { createCachedImport } from "../createCachedImport"; -export const calendarLoaders = { +const calendarLoaders = { applecalendar: createCachedImport(() => import("../../applecalendar")), caldavcalendar: createCachedImport(() => import("../../caldavcalendar")), googlecalendar: createCachedImport(() => import("../../googlecalendar")), @@ -14,3 +14,5 @@ export const calendarLoaders = { }; export type CalendarLoaderKey = keyof typeof calendarLoaders; + +export default calendarLoaders; diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 7e43e93ae4b28f..5c2b6d45f2d576 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -2,7 +2,7 @@ import logger from "@calcom/lib/logger"; import type { Calendar, CalendarClass } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; -import { calendarLoaders } from "./calendars/calendarLoaders"; +import calendarLoaders from "./calendars/calendarLoaders"; import type { CalendarLoaderKey } from "./calendars/calendarLoaders"; interface CalendarApp { diff --git a/packages/app-store/_utils/videos/getVideoApps.ts b/packages/app-store/_utils/videos/getVideoApps.ts new file mode 100644 index 00000000000000..aefb1ccb9ec278 --- /dev/null +++ b/packages/app-store/_utils/videos/getVideoApps.ts @@ -0,0 +1,100 @@ +import logger from "@calcom/lib/logger"; +import { getPiiFreeCredential } from "@calcom/lib/piiFreeData"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import type { App } from "@calcom/types/App"; +import type { AppMeta } from "@calcom/types/App"; +import type { CredentialForCalendarService } from "@calcom/types/Credential"; + +import type { EventLocationType } from "../../locations"; +import { videoAppsMetaData } from "./videoAppsMetaData"; + +export type LocationOption = { + label: string; + value: EventLocationType["type"]; + icon?: string; + disabled?: boolean; +}; + +export type CredentialDataWithTeamName = CredentialForCalendarService & { + team?: { + name: string; + } | null; +}; + +const VIDEO_APPS_MAP = Object.keys(videoAppsMetaData).reduce((store, key) => { + const metadata = videoAppsMetaData[key as keyof typeof videoAppsMetaData] as AppMeta; + + store[key] = metadata; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete store[key]["/*"]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete store[key]["__createdUsingCli"]; + return store; +}, {} as Record); + +const ALL_VIDEO_APPS = Object.values(VIDEO_APPS_MAP); + +export const getVideoApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => { + const apps = ALL_VIDEO_APPS.reduce((reducedArray, appMeta) => { + const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug); + + if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; + + let locationOption: LocationOption | null = null; + + /** If the app is a globally installed one, let's inject it's key */ + if (appMeta.isGlobal) { + const credential = { + id: 0, + type: appMeta.type, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + key: appMeta.key!, + userId: 0, + user: { email: "" }, + teamId: null, + appId: appMeta.slug, + invalid: false, + delegatedTo: null, + delegatedToId: null, + delegationCredentialId: null, + team: { + name: "Global", + }, + }; + logger.debug( + `${appMeta.type} is a global app, injecting credential`, + safeStringify(getPiiFreeCredential(credential)) + ); + appCredentials.push(credential); + } + + /** Check if app has location option AND add it if user has credentials for it */ + if (appCredentials.length > 0 && appMeta?.appData?.location) { + locationOption = { + value: appMeta.appData.location.type, + label: appMeta.appData.location.label || "No label set", + disabled: false, + }; + } + + const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; + + reducedArray.push({ + ...appMeta, + /** + * @deprecated use `credentials` + */ + credential, + credentials: appCredentials, + /** Option to display in `location` field while editing event types */ + locationOption, + }); + + return reducedArray; + }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); + + return apps; +}; diff --git a/packages/app-store/_utils/videos/videoAppsMetaData.ts b/packages/app-store/_utils/videos/videoAppsMetaData.ts new file mode 100644 index 00000000000000..117e5e11c09bc8 --- /dev/null +++ b/packages/app-store/_utils/videos/videoAppsMetaData.ts @@ -0,0 +1,14 @@ +import type { AppMeta } from "@calcom/types/App"; + +import { getNormalizedAppMetadata } from "../../getNormalizedAppMetadata"; +import { videoAppsMetadata as rawVideoAppsMetadata } from "../../videoApps.metadata.generated"; + +type RawVideoAppsMetaData = typeof rawVideoAppsMetadata; +type VideoAppsMetaData = { + [key in keyof RawVideoAppsMetaData]: Omit & { dirName: string }; +}; + +export const videoAppsMetaData = {} as VideoAppsMetaData; +for (const [key, value] of Object.entries(rawVideoAppsMetadata)) { + videoAppsMetaData[key as keyof typeof videoAppsMetaData] = getNormalizedAppMetadata(value); +} diff --git a/packages/app-store/_utils/videos/videoLoaders.ts b/packages/app-store/_utils/videos/videoLoaders.ts index 5bb61ae51d905a..371970dd3735b3 100644 --- a/packages/app-store/_utils/videos/videoLoaders.ts +++ b/packages/app-store/_utils/videos/videoLoaders.ts @@ -1,6 +1,6 @@ import { createCachedImport } from "../createCachedImport"; -export const videoLoaders = { +const videoLoaders = { dailyvideo: createCachedImport(() => import("../../dailyvideo")), googlevideo: createCachedImport(() => import("../../googlevideo")), huddle01video: createCachedImport(() => import("../../huddle01video")), @@ -15,3 +15,5 @@ export const videoLoaders = { }; export type VideoLoaderKey = keyof typeof videoLoaders; + +export default videoLoaders; diff --git a/packages/app-store/videoApps.metadata.generated.ts b/packages/app-store/videoApps.metadata.generated.ts new file mode 100644 index 00000000000000..5ae9954d21b433 --- /dev/null +++ b/packages/app-store/videoApps.metadata.generated.ts @@ -0,0 +1,65 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +import campfire_config_json from "./campfire/config.json"; +import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata"; +import demodesk_config_json from "./demodesk/config.json"; +import dialpad_config_json from "./dialpad/config.json"; +import discord_config_json from "./discord/config.json"; +import eightxeight_config_json from "./eightxeight/config.json"; +import element_call_config_json from "./element-call/config.json"; +import facetime_config_json from "./facetime/config.json"; +import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; +import horizon_workrooms_config_json from "./horizon-workrooms/config.json"; +import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; +import jelly_config_json from "./jelly/config.json"; +import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; +import mirotalk_config_json from "./mirotalk/config.json"; +import nextcloudtalk_config_json from "./nextcloudtalk/config.json"; +import office365video_config_json from "./office365video/config.json"; +import ping_config_json from "./ping/config.json"; +import riverside_config_json from "./riverside/config.json"; +import roam_config_json from "./roam/config.json"; +import salesroom_config_json from "./salesroom/config.json"; +import shimmervideo_config_json from "./shimmervideo/config.json"; +import sirius_video_config_json from "./sirius_video/config.json"; +import skype_config_json from "./skype/config.json"; +import sylapsvideo_config_json from "./sylapsvideo/config.json"; +import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata"; +import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json"; +import webex_config_json from "./webex/config.json"; +import whereby_config_json from "./whereby/config.json"; +import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata"; + +export const videoAppsMetadata = { + campfire: campfire_config_json, + dailyvideo: dailyvideo__metadata_ts, + demodesk: demodesk_config_json, + dialpad: dialpad_config_json, + discord: discord_config_json, + eightxeight: eightxeight_config_json, + "element-call": element_call_config_json, + facetime: facetime_config_json, + googlevideo: googlevideo__metadata_ts, + "horizon-workrooms": horizon_workrooms_config_json, + huddle01video: huddle01video__metadata_ts, + jelly: jelly_config_json, + jitsivideo: jitsivideo__metadata_ts, + mirotalk: mirotalk_config_json, + nextcloudtalk: nextcloudtalk_config_json, + office365video: office365video_config_json, + ping: ping_config_json, + riverside: riverside_config_json, + roam: roam_config_json, + salesroom: salesroom_config_json, + shimmervideo: shimmervideo_config_json, + sirius_video: sirius_video_config_json, + skype: skype_config_json, + sylapsvideo: sylapsvideo_config_json, + tandemvideo: tandemvideo__metadata_ts, + "event-type-location-video-static": event_type_location_video_static_config_json, + webex: webex_config_json, + whereby: whereby_config_json, + zoomvideo: zoomvideo__metadata_ts, +}; diff --git a/packages/lib/EventManager.ts b/packages/lib/EventManager.ts index a244eee2dcff73..3c44190a97e84b 100644 --- a/packages/lib/EventManager.ts +++ b/packages/lib/EventManager.ts @@ -4,7 +4,9 @@ import { cloneDeep, merge } from "lodash"; import { v5 as uuidv5 } from "uuid"; import type { z } from "zod"; +import { getCalendarApps } from "@calcom/app-store/_utils/calendars/getCalendarApps"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; +import { getVideoApps } from "@calcom/app-store/_utils/videos/getVideoApps"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvideo/zod"; import { getLocationFromApp, MeetLocationType, MSTeamsLocationType } from "@calcom/app-store/locations"; @@ -149,7 +151,8 @@ export default class EventManager { app.credentials.map((creds) => ({ ...creds, appName: app.name })) ); // This includes all calendar-related apps, traditional calendars such as Google Calendar - this.calendarCredentials = appCredentials + this.calendarCredentials = getCalendarApps(user.credentials, true) + .flatMap((app) => app.credentials.map((creds) => ({ ...creds, appName: app.name }))) .filter( // Backwards compatibility until CRM manager is implemented (cred) => cred.type.endsWith("_calendar") && !cred.type.includes("other_calendar") @@ -162,7 +165,8 @@ export default class EventManager { // Also, those credentials have consistent permission for all the members avoiding the scenario where user doesn't give all permissions .sort(delegatedCredentialFirst); - this.videoCredentials = appCredentials + this.videoCredentials = getVideoApps(user.credentials, true) + .flatMap((app) => app.credentials.map((creds) => ({ ...creds, appName: app.name }))) .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. // Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order diff --git a/packages/lib/server/getDefaultLocations.ts b/packages/lib/server/getDefaultLocations.ts index 275647a6ff2ad8..6098cc26887148 100644 --- a/packages/lib/server/getDefaultLocations.ts +++ b/packages/lib/server/getDefaultLocations.ts @@ -1,6 +1,6 @@ import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; +import { getVideoApps } from "@calcom/app-store/_utils/videos/getVideoApps"; import { DailyLocationType } from "@calcom/app-store/locations"; -import getApps from "@calcom/app-store/utils"; import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials"; import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; import type { EventTypeLocation } from "@calcom/prisma/zod/custom/eventtype"; @@ -20,7 +20,7 @@ export async function getDefaultLocations(user: User): Promise app.slug === defaultConferencingData.appSlug )[0]; // There is only one possible install here so index [0] is the one we are looking for ; const locationType = foundApp?.locationOption?.value ?? DailyLocationType; // Default to Daily if no location type is found diff --git a/packages/lib/videoClient.ts b/packages/lib/videoClient.ts index b8ba35422fe908..9c17ed61091491 100644 --- a/packages/lib/videoClient.ts +++ b/packages/lib/videoClient.ts @@ -1,7 +1,7 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; -import { videoLoaders } from "@calcom/app-store/_utils/videos/videoLoaders"; +import videoLoaders from "@calcom/app-store/_utils/videos/videoLoaders"; import type { VideoLoaderKey } from "@calcom/app-store/_utils/videos/videoLoaders"; import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys"; import { DailyLocationType } from "@calcom/app-store/locations"; diff --git a/packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts b/packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts index 761b596a53194d..b115dde2da6756 100644 --- a/packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts +++ b/packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts @@ -1,6 +1,6 @@ import z from "zod"; -import getApps from "@calcom/app-store/utils"; +import { getVideoApps } from "@calcom/app-store/_utils/videos/getVideoApps"; import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials"; import { prisma } from "@calcom/prisma"; import { userMetadata } from "@calcom/prisma/zod-utils"; @@ -25,7 +25,7 @@ export const updateUserDefaultConferencingAppHandler = async ({ // getApps need credentials with service account key // We aren't returning the credential, so we are fine with the service account key const credentials = await getUsersCredentialsIncludeServiceAccountKey(ctx.user); - const foundApp = getApps(credentials, true).filter((app) => app.slug === input.appSlug)[0]; + const foundApp = getVideoApps(credentials, true).filter((app) => app.slug === input.appSlug)[0]; const appLocation = foundApp?.appData?.location; if (!foundApp || !appLocation) throw new TRPCError({ code: "BAD_REQUEST", message: "App not installed" }); diff --git a/tests/libs/__mocks__/calendarApps.ts b/tests/libs/__mocks__/calendarApps.ts index 5df70cc61f5b6d..bc2261c86b5c3b 100644 --- a/tests/libs/__mocks__/calendarApps.ts +++ b/tests/libs/__mocks__/calendarApps.ts @@ -1,7 +1,7 @@ import { beforeEach, vi } from "vitest"; import { mockReset, mockDeep } from "vitest-mock-extended"; -import type { calendarLoaders } from "@calcom/app-store/_utils/calendars/calendarLoaders"; +import type * as calendarLoaders from "@calcom/app-store/_utils/calendars/calendarLoaders"; vi.mock("@calcom/app-store/_utils/calendars/calendarLoaders", () => calendarAppsMock); @@ -9,10 +9,12 @@ beforeEach(() => { mockReset(calendarAppsMock); }); -export const calendarAppsMock = mockDeep({ +const calendarAppsMock = mockDeep({ fallbackMockImplementation: () => { throw new Error( "Unimplemented calendarAppsMock. You seem to have not mocked the app that you are trying to use" ); }, }); + +export default calendarAppsMock; diff --git a/tests/libs/__mocks__/videoApps.ts b/tests/libs/__mocks__/videoApps.ts index 86904f86a0dde5..5c4110fd9dae99 100644 --- a/tests/libs/__mocks__/videoApps.ts +++ b/tests/libs/__mocks__/videoApps.ts @@ -1,7 +1,7 @@ import { beforeEach, vi } from "vitest"; import { mockReset, mockDeep } from "vitest-mock-extended"; -import type { videoLoaders } from "@calcom/app-store/_utils/videos/videoLoaders"; +import type * as videoLoaders from "@calcom/app-store/_utils/videos/videoLoaders"; vi.mock("@calcom/app-store/_utils/videos/videoLoaders", () => videoAppsMock); @@ -9,10 +9,12 @@ beforeEach(() => { mockReset(videoAppsMock); }); -export const videoAppsMock = mockDeep({ +const videoAppsMock = mockDeep({ fallbackMockImplementation: () => { throw new Error( "Unimplemented videoAppsMock. You seem to have not mocked the app that you are trying to use" ); }, }); + +export default videoAppsMock; From 19f9aa52cdcbb12bdb2a2ca1e22e0c32762dcb62 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Fri, 22 Aug 2025 16:49:09 +0800 Subject: [PATCH 14/22] test: mock default export instead of named export. --- .../utils/bookingScenario/bookingScenario.ts | 24 ++++++++++--------- .../_utils/payments/paymentLoaders.ts | 4 +++- packages/lib/getConnectedApps.ts | 2 +- packages/lib/payment/deletePayment.ts | 2 +- packages/lib/payment/handlePayment.ts | 2 +- packages/lib/payment/handlePaymentRefund.ts | 2 +- .../trpc/server/routers/viewer/payments.tsx | 2 +- .../viewer/payments/chargeCard.handler.ts | 2 +- tests/libs/__mocks__/paymentApps.ts | 4 +++- 9 files changed, 25 insertions(+), 19 deletions(-) diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index ca3831016e4c2e..ff4ecf77946eb0 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -1,7 +1,7 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; import calendarAppsMock from "../../../../../tests/libs/__mocks__/calendarApps"; import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; -import { paymentAppsMock } from "../../../../../tests/libs/__mocks__/paymentApps"; +import paymentAppsMock from "../../../../../tests/libs/__mocks__/paymentApps"; import prismock from "../../../../../tests/libs/__mocks__/prisma"; import videoAppsMock from "../../../../../tests/libs/__mocks__/videoApps"; @@ -2068,17 +2068,19 @@ export function mockPaymentApp({ const { paymentUid, externalId, MockPaymentService } = getMockPaymentService(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - paymentAppsMock[appStoreLookupKey as keyof typeof paymentAppsMock].mockImplementation(() => { - return new Promise((resolve) => { - resolve({ - lib: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error FIXME - PaymentService: MockPaymentService, - }, + paymentAppsMock.default[appStoreLookupKey as keyof typeof paymentAppsMock.default].mockImplementation( + () => { + return new Promise((resolve) => { + resolve({ + lib: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error FIXME + PaymentService: MockPaymentService, + }, + }); }); - }); - }); + } + ); return { paymentUid, diff --git a/packages/app-store/_utils/payments/paymentLoaders.ts b/packages/app-store/_utils/payments/paymentLoaders.ts index 4d08506804d00d..f186002e1e9a8d 100644 --- a/packages/app-store/_utils/payments/paymentLoaders.ts +++ b/packages/app-store/_utils/payments/paymentLoaders.ts @@ -1,6 +1,6 @@ import { createCachedImport } from "../createCachedImport"; -export const paymentLoaders = { +const paymentLoaders = { alby: createCachedImport(() => import("../../alby")), paypal: createCachedImport(() => import("../../paypal")), stripepayment: createCachedImport(() => import("../../stripepayment")), @@ -14,3 +14,5 @@ if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { } export type PaymentLoaderKey = keyof typeof paymentLoaders | "mock-payment-app"; + +export default paymentLoaders; diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index 306418264b7acf..00865d4d46bac9 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -1,7 +1,7 @@ import type { Prisma } from "@prisma/client"; import type { TDependencyData } from "@calcom/app-store/_appRegistry"; -import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { CredentialOwner } from "@calcom/app-store/types"; import { getAppFromSlug } from "@calcom/app-store/utils"; diff --git a/packages/lib/payment/deletePayment.ts b/packages/lib/payment/deletePayment.ts index 690fb6324a1860..ef4ccd35d21538 100644 --- a/packages/lib/payment/deletePayment.ts +++ b/packages/lib/payment/deletePayment.ts @@ -1,6 +1,6 @@ import type { Payment, Prisma } from "@prisma/client"; -import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { AppCategories } from "@calcom/prisma/enums"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 8c9f8d7d5fcaa1..dd3bb2474f043d 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -1,6 +1,6 @@ import type { AppCategories, Prisma } from "@prisma/client"; -import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; import type { CompleteEventType } from "@calcom/prisma/zod"; import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils"; diff --git a/packages/lib/payment/handlePaymentRefund.ts b/packages/lib/payment/handlePaymentRefund.ts index ca65346b461c5d..09b2d3a030f014 100644 --- a/packages/lib/payment/handlePaymentRefund.ts +++ b/packages/lib/payment/handlePaymentRefund.ts @@ -1,6 +1,6 @@ import type { Payment, Prisma } from "@prisma/client"; -import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { AppCategories } from "@calcom/prisma/enums"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index ca2f431c968cb6..1e04d6cf9865c0 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -1,6 +1,6 @@ import { z } from "zod"; -import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails"; diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index 1c1d04997a0d51..db118dfbf6f765 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -1,4 +1,4 @@ -import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails"; diff --git a/tests/libs/__mocks__/paymentApps.ts b/tests/libs/__mocks__/paymentApps.ts index e0bf9986166361..c0476471988718 100644 --- a/tests/libs/__mocks__/paymentApps.ts +++ b/tests/libs/__mocks__/paymentApps.ts @@ -9,10 +9,12 @@ beforeEach(() => { mockReset(paymentAppsMock); }); -export const paymentAppsMock = mockDeep({ +const paymentAppsMock = mockDeep({ fallbackMockImplementation: () => { throw new Error( "Unimplemented paymentAppsMock. You seem to have not mocked the app that you are trying to use" ); }, }); + +export default paymentAppsMock; From 76bc398c5185c9f58d3e7624157fae1b5350d468 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Fri, 22 Aug 2025 16:51:09 +0800 Subject: [PATCH 15/22] test: update import to use namespace for `paymentLoaders` mock --- tests/libs/__mocks__/paymentApps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/libs/__mocks__/paymentApps.ts b/tests/libs/__mocks__/paymentApps.ts index c0476471988718..b5d2f0d172b553 100644 --- a/tests/libs/__mocks__/paymentApps.ts +++ b/tests/libs/__mocks__/paymentApps.ts @@ -1,7 +1,7 @@ import { beforeEach, vi } from "vitest"; import { mockReset, mockDeep } from "vitest-mock-extended"; -import type { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type * as paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => paymentAppsMock); From 5c7df2fa819e84b41c33dfc4e537a4119930a269 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Fri, 22 Aug 2025 16:58:21 +0800 Subject: [PATCH 16/22] chore: update `paymentAppsMock` implementation to use @ts-expect-error for type safety --- apps/web/test/utils/bookingScenario/bookingScenario.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index ff4ecf77946eb0..7ef039fabd99ab 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -2066,9 +2066,9 @@ export function mockPaymentApp({ }) { appStoreLookupKey = appStoreLookupKey || metadataLookupKey; const { paymentUid, externalId, MockPaymentService } = getMockPaymentService(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore paymentAppsMock.default[appStoreLookupKey as keyof typeof paymentAppsMock.default].mockImplementation( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error FIXME () => { return new Promise((resolve) => { resolve({ From ddb8365a2220525ca711742f849bb8d843e4f1d1 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Fri, 22 Aug 2025 20:46:13 +0800 Subject: [PATCH 17/22] refactor: add new utility functions for credential preparation and metadata sanitization --- .../_utils/calendars/getCalendarApps.ts | 100 +----------------- .../_utils/prepareAppsWithCredentials.ts | 84 +++++++++++++++ .../app-store/_utils/sanitizeAppsMetadata.ts | 19 ++++ .../app-store/_utils/videos/getVideoApps.ts | 100 +----------------- packages/app-store/utils.ts | 66 +----------- 5 files changed, 118 insertions(+), 251 deletions(-) create mode 100644 packages/app-store/_utils/prepareAppsWithCredentials.ts create mode 100644 packages/app-store/_utils/sanitizeAppsMetadata.ts diff --git a/packages/app-store/_utils/calendars/getCalendarApps.ts b/packages/app-store/_utils/calendars/getCalendarApps.ts index 8a3f220bbbf7f3..dec8aae0bd169a 100644 --- a/packages/app-store/_utils/calendars/getCalendarApps.ts +++ b/packages/app-store/_utils/calendars/getCalendarApps.ts @@ -1,100 +1,10 @@ -import logger from "@calcom/lib/logger"; -import { getPiiFreeCredential } from "@calcom/lib/piiFreeData"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import type { App } from "@calcom/types/App"; -import type { AppMeta } from "@calcom/types/App"; -import type { CredentialForCalendarService } from "@calcom/types/Credential"; - -import type { EventLocationType } from "../../locations"; +import { prepareAppsWithCredentials } from "../prepareAppsWithCredentials"; +import type { CredentialDataWithTeamName } from "../prepareAppsWithCredentials"; +import { sanitizeAppsMetadata } from "../sanitizeAppsMetadata"; import { calendarAppsMetaData } from "./calendarAppsMetaData"; -export type LocationOption = { - label: string; - value: EventLocationType["type"]; - icon?: string; - disabled?: boolean; -}; - -export type CredentialDataWithTeamName = CredentialForCalendarService & { - team?: { - name: string; - } | null; -}; - -const CALENDAR_APPS_MAP = Object.keys(calendarAppsMetaData).reduce((store, key) => { - const metadata = calendarAppsMetaData[key as keyof typeof calendarAppsMetaData] as AppMeta; - - store[key] = metadata; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - delete store[key]["/*"]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - delete store[key]["__createdUsingCli"]; - return store; -}, {} as Record); - -const ALL_CALENDAR_APPS = Object.values(CALENDAR_APPS_MAP); +const ALL_CALENDAR_APPS = sanitizeAppsMetadata(calendarAppsMetaData); export const getCalendarApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => { - const apps = ALL_CALENDAR_APPS.reduce((reducedArray, appMeta) => { - const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug); - - if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; - - let locationOption: LocationOption | null = null; - - /** If the app is a globally installed one, let's inject it's key */ - if (appMeta.isGlobal) { - const credential = { - id: 0, - type: appMeta.type, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - key: appMeta.key!, - userId: 0, - user: { email: "" }, - teamId: null, - appId: appMeta.slug, - invalid: false, - delegatedTo: null, - delegatedToId: null, - delegationCredentialId: null, - team: { - name: "Global", - }, - }; - logger.debug( - `${appMeta.type} is a global app, injecting credential`, - safeStringify(getPiiFreeCredential(credential)) - ); - appCredentials.push(credential); - } - - /** Check if app has location option AND add it if user has credentials for it */ - if (appCredentials.length > 0 && appMeta?.appData?.location) { - locationOption = { - value: appMeta.appData.location.type, - label: appMeta.appData.location.label || "No label set", - disabled: false, - }; - } - - const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; - - reducedArray.push({ - ...appMeta, - /** - * @deprecated use `credentials` - */ - credential, - credentials: appCredentials, - /** Option to display in `location` field while editing event types */ - locationOption, - }); - - return reducedArray; - }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); - - return apps; + return prepareAppsWithCredentials(ALL_CALENDAR_APPS, credentials, filterOnCredentials); }; diff --git a/packages/app-store/_utils/prepareAppsWithCredentials.ts b/packages/app-store/_utils/prepareAppsWithCredentials.ts new file mode 100644 index 00000000000000..ea10d9f9cb03c9 --- /dev/null +++ b/packages/app-store/_utils/prepareAppsWithCredentials.ts @@ -0,0 +1,84 @@ +import logger from "@calcom/lib/logger"; +import { getPiiFreeCredential } from "@calcom/lib/piiFreeData"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import type { App } from "@calcom/types/App"; +import type { CredentialForCalendarService } from "@calcom/types/Credential"; + +import type { EventLocationType } from "../locations"; + +export type LocationOption = { + label: string; + value: EventLocationType["type"]; + icon?: string; + disabled?: boolean; +}; + +export type CredentialDataWithTeamName = CredentialForCalendarService & { + team?: { + name: string; + } | null; +}; + +export const prepareAppsWithCredentials = ( + apps: App[], + credentials: CredentialDataWithTeamName[], + filterOnCredentials?: boolean +) => { + return apps.reduce((reducedArray, appMeta) => { + const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug); + + if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; + + let locationOption: LocationOption | null = null; + + /** If the app is a globally installed one, let's inject it's key */ + if (appMeta.isGlobal) { + const credential = { + id: 0, + type: appMeta.type, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + key: appMeta.key!, + userId: 0, + user: { email: "" }, + teamId: null, + appId: appMeta.slug, + invalid: false, + delegatedTo: null, + delegatedToId: null, + delegationCredentialId: null, + team: { + name: "Global", + }, + }; + logger.debug( + `${appMeta.type} is a global app, injecting credential`, + safeStringify(getPiiFreeCredential(credential)) + ); + appCredentials.push(credential); + } + + /** Check if app has location option AND add it if user has credentials for it */ + if (appCredentials.length > 0 && appMeta?.appData?.location) { + locationOption = { + value: appMeta.appData.location.type, + label: appMeta.appData.location.label || "No label set", + disabled: false, + }; + } + + const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; + + reducedArray.push({ + ...appMeta, + /** + * @deprecated use `credentials` + */ + credential, + credentials: appCredentials, + /** Option to display in `location` field while editing event types */ + locationOption, + }); + + return reducedArray; + }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); +}; diff --git a/packages/app-store/_utils/sanitizeAppsMetadata.ts b/packages/app-store/_utils/sanitizeAppsMetadata.ts new file mode 100644 index 00000000000000..f56c0f93056c88 --- /dev/null +++ b/packages/app-store/_utils/sanitizeAppsMetadata.ts @@ -0,0 +1,19 @@ +import type { AppMeta } from "@calcom/types/App"; + +export const sanitizeAppsMetadata = (appsMetaData: T) => { + const appsMap = Object.keys(appsMetaData).reduce((store, key) => { + const metadata = appsMetaData[key as keyof T] as AppMeta; + + store[key] = metadata; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete store[key]["/*"]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete store[key]["__createdUsingCli"]; + return store; + }, {} as Record); + + return Object.values(appsMap); +}; diff --git a/packages/app-store/_utils/videos/getVideoApps.ts b/packages/app-store/_utils/videos/getVideoApps.ts index aefb1ccb9ec278..7193adc4ad4bb9 100644 --- a/packages/app-store/_utils/videos/getVideoApps.ts +++ b/packages/app-store/_utils/videos/getVideoApps.ts @@ -1,100 +1,10 @@ -import logger from "@calcom/lib/logger"; -import { getPiiFreeCredential } from "@calcom/lib/piiFreeData"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import type { App } from "@calcom/types/App"; -import type { AppMeta } from "@calcom/types/App"; -import type { CredentialForCalendarService } from "@calcom/types/Credential"; - -import type { EventLocationType } from "../../locations"; +import { prepareAppsWithCredentials } from "../prepareAppsWithCredentials"; +import type { CredentialDataWithTeamName } from "../prepareAppsWithCredentials"; +import { sanitizeAppsMetadata } from "../sanitizeAppsMetadata"; import { videoAppsMetaData } from "./videoAppsMetaData"; -export type LocationOption = { - label: string; - value: EventLocationType["type"]; - icon?: string; - disabled?: boolean; -}; - -export type CredentialDataWithTeamName = CredentialForCalendarService & { - team?: { - name: string; - } | null; -}; - -const VIDEO_APPS_MAP = Object.keys(videoAppsMetaData).reduce((store, key) => { - const metadata = videoAppsMetaData[key as keyof typeof videoAppsMetaData] as AppMeta; - - store[key] = metadata; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - delete store[key]["/*"]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - delete store[key]["__createdUsingCli"]; - return store; -}, {} as Record); - -const ALL_VIDEO_APPS = Object.values(VIDEO_APPS_MAP); +const ALL_VIDEO_APPS = sanitizeAppsMetadata(videoAppsMetaData); export const getVideoApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => { - const apps = ALL_VIDEO_APPS.reduce((reducedArray, appMeta) => { - const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug); - - if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; - - let locationOption: LocationOption | null = null; - - /** If the app is a globally installed one, let's inject it's key */ - if (appMeta.isGlobal) { - const credential = { - id: 0, - type: appMeta.type, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - key: appMeta.key!, - userId: 0, - user: { email: "" }, - teamId: null, - appId: appMeta.slug, - invalid: false, - delegatedTo: null, - delegatedToId: null, - delegationCredentialId: null, - team: { - name: "Global", - }, - }; - logger.debug( - `${appMeta.type} is a global app, injecting credential`, - safeStringify(getPiiFreeCredential(credential)) - ); - appCredentials.push(credential); - } - - /** Check if app has location option AND add it if user has credentials for it */ - if (appCredentials.length > 0 && appMeta?.appData?.location) { - locationOption = { - value: appMeta.appData.location.type, - label: appMeta.appData.location.label || "No label set", - disabled: false, - }; - } - - const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; - - reducedArray.push({ - ...appMeta, - /** - * @deprecated use `credentials` - */ - credential, - credentials: appCredentials, - /** Option to display in `location` field while editing event types */ - locationOption, - }); - - return reducedArray; - }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); - - return apps; + return prepareAppsWithCredentials(ALL_VIDEO_APPS, credentials, filterOnCredentials); }; diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index 0c48c600d0a70b..fe7ddaeee4587b 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -4,12 +4,12 @@ import type { AppCategories } from "@prisma/client"; // import appStore from "./index"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import type { EventLocationType } from "@calcom/app-store/locations"; -import logger from "@calcom/lib/logger"; -import { getPiiFreeCredential } from "@calcom/lib/piiFreeData"; -import { safeStringify } from "@calcom/lib/safeStringify"; import type { App, AppMeta } from "@calcom/types/App"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; +import { prepareAppsWithCredentials } from "./_utils/prepareAppsWithCredentials"; +import { sanitizeAppsMetadata } from "./_utils/sanitizeAppsMetadata"; + export * from "./_utils/getEventTypeAppData"; export type LocationOption = { @@ -39,70 +39,14 @@ export type CredentialDataWithTeamName = CredentialForCalendarService & { } | null; }; -export const ALL_APPS = Object.values(ALL_APPS_MAP); +export const ALL_APPS = sanitizeAppsMetadata(appStoreMetadata); /** * This should get all available apps to the user based on his saved * credentials, this should also get globally available apps. */ function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) { - const apps = ALL_APPS.reduce((reducedArray, appMeta) => { - const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug); - - if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; - - let locationOption: LocationOption | null = null; - - /** If the app is a globally installed one, let's inject it's key */ - if (appMeta.isGlobal) { - const credential = { - id: 0, - type: appMeta.type, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - key: appMeta.key!, - userId: 0, - user: { email: "" }, - teamId: null, - appId: appMeta.slug, - invalid: false, - delegatedTo: null, - delegatedToId: null, - delegationCredentialId: null, - team: { - name: "Global", - }, - }; - logger.debug( - `${appMeta.type} is a global app, injecting credential`, - safeStringify(getPiiFreeCredential(credential)) - ); - appCredentials.push(credential); - } - - /** Check if app has location option AND add it if user has credentials for it */ - if (appCredentials.length > 0 && appMeta?.appData?.location) { - locationOption = { - value: appMeta.appData.location.type, - label: appMeta.appData.location.label || "No label set", - disabled: false, - }; - } - - const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; - - reducedArray.push({ - ...appMeta, - /** - * @deprecated use `credentials` - */ - credential, - credentials: appCredentials, - /** Option to display in `location` field while editing event types */ - locationOption, - }); - - return reducedArray; - }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); + const apps = prepareAppsWithCredentials(ALL_APPS, credentials, filterOnCredentials); return apps; } From d079cb59595ebc741d09749479356e83669e049b Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Sat, 23 Aug 2025 10:32:15 +0800 Subject: [PATCH 18/22] refactor: streamline mock implementations by introducing `mockDeepHelper` utility --- tests/libs/__mocks__/app-store.ts | 14 +++++--------- tests/libs/__mocks__/calendarApps.ts | 14 +++++--------- tests/libs/__mocks__/mockDeepHelper.ts | 10 ++++++++++ tests/libs/__mocks__/paymentApps.ts | 14 +++++--------- tests/libs/__mocks__/videoApps.ts | 14 +++++--------- 5 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 tests/libs/__mocks__/mockDeepHelper.ts diff --git a/tests/libs/__mocks__/app-store.ts b/tests/libs/__mocks__/app-store.ts index bf4462b133b393..28c9b92f19f71c 100644 --- a/tests/libs/__mocks__/app-store.ts +++ b/tests/libs/__mocks__/app-store.ts @@ -1,20 +1,16 @@ import { beforeEach, vi } from "vitest"; -import { mockReset, mockDeep } from "vitest-mock-extended"; +import { mockReset } from "vitest-mock-extended"; -// FIXME is it okay to use the large type here? import type * as appStore from "@calcom/app-store"; +import { mockDeepHelper } from "./mockDeepHelper"; + +const appStoreMock = mockDeepHelper("appStoreMock"); + vi.mock("@calcom/app-store", () => appStoreMock); beforeEach(() => { mockReset(appStoreMock); }); -const appStoreMock = mockDeep({ - fallbackMockImplementation: () => { - throw new Error( - "Unimplemented appStoreMock. You seem to have not mocked the app that you are trying to use" - ); - }, -}); export default appStoreMock; diff --git a/tests/libs/__mocks__/calendarApps.ts b/tests/libs/__mocks__/calendarApps.ts index bc2261c86b5c3b..c781c596c2c0dc 100644 --- a/tests/libs/__mocks__/calendarApps.ts +++ b/tests/libs/__mocks__/calendarApps.ts @@ -1,20 +1,16 @@ import { beforeEach, vi } from "vitest"; -import { mockReset, mockDeep } from "vitest-mock-extended"; +import { mockReset } from "vitest-mock-extended"; import type * as calendarLoaders from "@calcom/app-store/_utils/calendars/calendarLoaders"; +import { mockDeepHelper } from "./mockDeepHelper"; + +const calendarAppsMock = mockDeepHelper("calendarAppsMock"); + vi.mock("@calcom/app-store/_utils/calendars/calendarLoaders", () => calendarAppsMock); beforeEach(() => { mockReset(calendarAppsMock); }); -const calendarAppsMock = mockDeep({ - fallbackMockImplementation: () => { - throw new Error( - "Unimplemented calendarAppsMock. You seem to have not mocked the app that you are trying to use" - ); - }, -}); - export default calendarAppsMock; diff --git a/tests/libs/__mocks__/mockDeepHelper.ts b/tests/libs/__mocks__/mockDeepHelper.ts new file mode 100644 index 00000000000000..cae2fc219b1067 --- /dev/null +++ b/tests/libs/__mocks__/mockDeepHelper.ts @@ -0,0 +1,10 @@ +import { mockDeep } from "vitest-mock-extended"; + +export const mockDeepHelper = (mockFnName = "") => + mockDeep({ + fallbackMockImplementation: () => { + throw new Error( + `Unimplemented ${mockFnName}. You seem to have not mocked the app that you are trying to use` + ); + }, + }); diff --git a/tests/libs/__mocks__/paymentApps.ts b/tests/libs/__mocks__/paymentApps.ts index b5d2f0d172b553..43a954b0a75c89 100644 --- a/tests/libs/__mocks__/paymentApps.ts +++ b/tests/libs/__mocks__/paymentApps.ts @@ -1,20 +1,16 @@ import { beforeEach, vi } from "vitest"; -import { mockReset, mockDeep } from "vitest-mock-extended"; +import { mockReset } from "vitest-mock-extended"; import type * as paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; +import { mockDeepHelper } from "./mockDeepHelper"; + +const paymentAppsMock = mockDeepHelper("paymentAppsMock"); + vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => paymentAppsMock); beforeEach(() => { mockReset(paymentAppsMock); }); -const paymentAppsMock = mockDeep({ - fallbackMockImplementation: () => { - throw new Error( - "Unimplemented paymentAppsMock. You seem to have not mocked the app that you are trying to use" - ); - }, -}); - export default paymentAppsMock; diff --git a/tests/libs/__mocks__/videoApps.ts b/tests/libs/__mocks__/videoApps.ts index 5c4110fd9dae99..6dd6c089a5aea5 100644 --- a/tests/libs/__mocks__/videoApps.ts +++ b/tests/libs/__mocks__/videoApps.ts @@ -1,20 +1,16 @@ import { beforeEach, vi } from "vitest"; -import { mockReset, mockDeep } from "vitest-mock-extended"; +import { mockReset } from "vitest-mock-extended"; import type * as videoLoaders from "@calcom/app-store/_utils/videos/videoLoaders"; +import { mockDeepHelper } from "./mockDeepHelper"; + +const videoAppsMock = mockDeepHelper("videoAppsMock"); + vi.mock("@calcom/app-store/_utils/videos/videoLoaders", () => videoAppsMock); beforeEach(() => { mockReset(videoAppsMock); }); -const videoAppsMock = mockDeep({ - fallbackMockImplementation: () => { - throw new Error( - "Unimplemented videoAppsMock. You seem to have not mocked the app that you are trying to use" - ); - }, -}); - export default videoAppsMock; From b53f30f8781e749070cd35fd8a763fe0aa282f07 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Sat, 23 Aug 2025 10:38:51 +0800 Subject: [PATCH 19/22] refactor: rename mock implementations for calendar, payment, and video loaders --- .../utils/bookingScenario/bookingScenario.ts | 132 +++++++++--------- ...calendarApps.ts => calendarLoadersMock.ts} | 8 +- .../{paymentApps.ts => paymentLoadersMock.ts} | 8 +- .../{videoApps.ts => videoLoadersMock.ts} | 8 +- 4 files changed, 79 insertions(+), 77 deletions(-) rename tests/libs/__mocks__/{calendarApps.ts => calendarLoadersMock.ts} (63%) rename tests/libs/__mocks__/{paymentApps.ts => paymentLoadersMock.ts} (64%) rename tests/libs/__mocks__/{videoApps.ts => videoLoadersMock.ts} (66%) diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 7ef039fabd99ab..766d82d73fbc29 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -1,9 +1,9 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; -import calendarAppsMock from "../../../../../tests/libs/__mocks__/calendarApps"; +import calendarLoadersMock from "../../../../../tests/libs/__mocks__/calendarLoadersMock"; import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; -import paymentAppsMock from "../../../../../tests/libs/__mocks__/paymentApps"; +import paymentLoadersMock from "../../../../../tests/libs/__mocks__/paymentLoadersMock"; import prismock from "../../../../../tests/libs/__mocks__/prisma"; -import videoAppsMock from "../../../../../tests/libs/__mocks__/videoApps"; +import videoLoadersMock from "../../../../../tests/libs/__mocks__/videoLoadersMock"; import type { BookingReference, Attendee, Booking, Membership } from "@prisma/client"; import type { Prisma } from "@prisma/client"; @@ -1771,7 +1771,7 @@ export function mockCalendar( const getAvailabilityCalls: GetAvailabilityMethodMockCall[] = []; const app = calendarAppsMetaData[metadataLookupKey as keyof typeof calendarAppsMetaData]; - const appMock = calendarAppsMock.default[appStoreLookupKey as keyof typeof calendarAppsMock.default]; + const appMock = calendarLoadersMock.default[appStoreLookupKey as keyof typeof calendarLoadersMock.default]; appMock && `mockResolvedValue` in appMock && @@ -1955,67 +1955,69 @@ export function mockVideoApp({ const updateMeetingCalls: any[] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any const deleteMeetingCalls: any[] = []; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error FIXME - videoAppsMock.default[appStoreLookupKey as keyof typeof videoAppsMock.default].mockImplementation(() => { - return new Promise((resolve) => { - resolve({ - lib: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error FIXME - VideoApiAdapter: (credential) => { - return { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createMeeting: (...rest: any[]) => { - if (creationCrash) { - throw new Error("MockVideoApiAdapter.createMeeting fake error"); - } - createMeetingCalls.push({ - credential, - args: rest, - }); - - return Promise.resolve({ - type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, - ...videoMeetingData, - }); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - updateMeeting: async (...rest: any[]) => { - if (updationCrash) { - throw new Error("MockVideoApiAdapter.updateMeeting fake error"); - } - const [bookingRef, calEvent] = rest; - updateMeetingCalls.push({ - credential, - args: rest, - }); - if (!bookingRef.type) { - throw new Error("bookingRef.type is not defined"); - } - if (!calEvent.organizer) { - throw new Error("calEvent.organizer is not defined"); - } - log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent })); - return Promise.resolve({ - type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, - ...videoMeetingData, - }); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - deleteMeeting: async (...rest: any[]) => { - log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest)); - deleteMeetingCalls.push({ - credential, - args: rest, - }); - }, - }; + videoLoadersMock.default[appStoreLookupKey as keyof typeof videoLoadersMock.default].mockImplementation( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error FIXME + () => { + return new Promise((resolve) => { + resolve({ + lib: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error FIXME + VideoApiAdapter: (credential) => { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createMeeting: (...rest: any[]) => { + if (creationCrash) { + throw new Error("MockVideoApiAdapter.createMeeting fake error"); + } + createMeetingCalls.push({ + credential, + args: rest, + }); + + return Promise.resolve({ + type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, + ...videoMeetingData, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateMeeting: async (...rest: any[]) => { + if (updationCrash) { + throw new Error("MockVideoApiAdapter.updateMeeting fake error"); + } + const [bookingRef, calEvent] = rest; + updateMeetingCalls.push({ + credential, + args: rest, + }); + if (!bookingRef.type) { + throw new Error("bookingRef.type is not defined"); + } + if (!calEvent.organizer) { + throw new Error("calEvent.organizer is not defined"); + } + log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent })); + return Promise.resolve({ + type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, + ...videoMeetingData, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deleteMeeting: async (...rest: any[]) => { + log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest)); + deleteMeetingCalls.push({ + credential, + args: rest, + }); + }, + }; + }, }, - }, + }); }); - }); - }); + } + ); return { createMeetingCalls, updateMeetingCalls, @@ -2066,7 +2068,7 @@ export function mockPaymentApp({ }) { appStoreLookupKey = appStoreLookupKey || metadataLookupKey; const { paymentUid, externalId, MockPaymentService } = getMockPaymentService(); - paymentAppsMock.default[appStoreLookupKey as keyof typeof paymentAppsMock.default].mockImplementation( + paymentLoadersMock.default[appStoreLookupKey as keyof typeof paymentLoadersMock.default].mockImplementation( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error FIXME () => { @@ -2098,7 +2100,7 @@ export function mockErrorOnVideoMeetingCreation({ appStoreLookupKey = appStoreLookupKey || metadataLookupKey; // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - videoAppsMock.default[appStoreLookupKey].mockImplementation(() => { + videoLoadersMock.default[appStoreLookupKey].mockImplementation(() => { return new Promise((resolve) => { resolve({ lib: { diff --git a/tests/libs/__mocks__/calendarApps.ts b/tests/libs/__mocks__/calendarLoadersMock.ts similarity index 63% rename from tests/libs/__mocks__/calendarApps.ts rename to tests/libs/__mocks__/calendarLoadersMock.ts index c781c596c2c0dc..d649a79afa1bee 100644 --- a/tests/libs/__mocks__/calendarApps.ts +++ b/tests/libs/__mocks__/calendarLoadersMock.ts @@ -5,12 +5,12 @@ import type * as calendarLoaders from "@calcom/app-store/_utils/calendars/calend import { mockDeepHelper } from "./mockDeepHelper"; -const calendarAppsMock = mockDeepHelper("calendarAppsMock"); +const calendarLoadersMock = mockDeepHelper("calendarLoadersMock"); -vi.mock("@calcom/app-store/_utils/calendars/calendarLoaders", () => calendarAppsMock); +vi.mock("@calcom/app-store/_utils/calendars/calendarLoaders", () => calendarLoadersMock); beforeEach(() => { - mockReset(calendarAppsMock); + mockReset(calendarLoadersMock); }); -export default calendarAppsMock; +export default calendarLoadersMock; diff --git a/tests/libs/__mocks__/paymentApps.ts b/tests/libs/__mocks__/paymentLoadersMock.ts similarity index 64% rename from tests/libs/__mocks__/paymentApps.ts rename to tests/libs/__mocks__/paymentLoadersMock.ts index 43a954b0a75c89..809aab2203c121 100644 --- a/tests/libs/__mocks__/paymentApps.ts +++ b/tests/libs/__mocks__/paymentLoadersMock.ts @@ -5,12 +5,12 @@ import type * as paymentLoaders from "@calcom/app-store/_utils/payments/paymentL import { mockDeepHelper } from "./mockDeepHelper"; -const paymentAppsMock = mockDeepHelper("paymentAppsMock"); +const paymentLoadersMock = mockDeepHelper("paymentLoadersMock"); -vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => paymentAppsMock); +vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => paymentLoadersMock); beforeEach(() => { - mockReset(paymentAppsMock); + mockReset(paymentLoadersMock); }); -export default paymentAppsMock; +export default paymentLoadersMock; diff --git a/tests/libs/__mocks__/videoApps.ts b/tests/libs/__mocks__/videoLoadersMock.ts similarity index 66% rename from tests/libs/__mocks__/videoApps.ts rename to tests/libs/__mocks__/videoLoadersMock.ts index 6dd6c089a5aea5..66168d1ba6b831 100644 --- a/tests/libs/__mocks__/videoApps.ts +++ b/tests/libs/__mocks__/videoLoadersMock.ts @@ -5,12 +5,12 @@ import type * as videoLoaders from "@calcom/app-store/_utils/videos/videoLoaders import { mockDeepHelper } from "./mockDeepHelper"; -const videoAppsMock = mockDeepHelper("videoAppsMock"); +const videoLoadersMock = mockDeepHelper("videoLoadersMock"); -vi.mock("@calcom/app-store/_utils/videos/videoLoaders", () => videoAppsMock); +vi.mock("@calcom/app-store/_utils/videos/videoLoaders", () => videoLoadersMock); beforeEach(() => { - mockReset(videoAppsMock); + mockReset(videoLoadersMock); }); -export default videoAppsMock; +export default videoLoadersMock; From 02da408024b764aca3f84fa0a307ce556209e23a Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Sat, 23 Aug 2025 21:36:34 +0800 Subject: [PATCH 20/22] refactor: replace payment setup logic with dedicated `setupPaymentService` function --- packages/lib/getConnectedApps.ts | 12 ++---------- packages/lib/setupPaymentService.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 packages/lib/setupPaymentService.ts diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index 00865d4d46bac9..db3e4bf8cebc2c 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -1,8 +1,6 @@ import type { Prisma } from "@prisma/client"; import type { TDependencyData } from "@calcom/app-store/_appRegistry"; -import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; -import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { CredentialOwner } from "@calcom/app-store/types"; import { getAppFromSlug } from "@calcom/app-store/utils"; import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; @@ -13,9 +11,9 @@ import type { PrismaClient } from "@calcom/prisma"; import type { User } from "@calcom/prisma/client"; import type { AppCategories } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -import type { PaymentApp } from "@calcom/types/PaymentService"; import { buildNonDelegationCredentials } from "./delegationCredential/clientAndServer"; +import { setupPaymentService } from "./setupPaymentService"; export type ConnectedApps = Awaited>; type InputSchema = { @@ -185,13 +183,7 @@ export async function getConnectedApps({ // undefined it means that app don't require app/setup/page let isSetupAlready = undefined; if (credential && app.categories.includes("payment")) { - // @ts-expect-error FIXME - const paymentApp = (await paymentLoaders[app.dirName as PaymentLoaderKey]?.()) as PaymentApp | null; - if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { - const PaymentService = paymentApp.lib.PaymentService; - const paymentInstance = new PaymentService(credential); - isSetupAlready = paymentInstance.isSetupAlready(); - } + isSetupAlready = await setupPaymentService(); } let dependencyData: TDependencyData = []; diff --git a/packages/lib/setupPaymentService.ts b/packages/lib/setupPaymentService.ts new file mode 100644 index 00000000000000..b8fd2c5a392b7b --- /dev/null +++ b/packages/lib/setupPaymentService.ts @@ -0,0 +1,16 @@ +import paymentLoaders, { type PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type { PaymentApp } from "@calcom/types/PaymentService"; + +export const setupPaymentService = async () => { + console.log("Checking payment setup for app", app.name); + // @ts-expect-error FIXME + const paymentApp = (await paymentLoaders[app.dirName as PaymentLoaderKey]?.()) as PaymentApp | null; + if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { + const PaymentService = paymentApp.lib.PaymentService; + const paymentInstance = new PaymentService(credential); + + const isPaymentSetupAlready = paymentInstance.isSetupAlready(); + + return isPaymentSetupAlready; + } +}; From ffb0fcb8b5a9b221097089fb4b59d4dc808a582d Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Sat, 23 Aug 2025 21:56:12 +0800 Subject: [PATCH 21/22] fix: update `setupPaymentService` to accept app and credential parameters --- packages/lib/getConnectedApps.ts | 2 +- packages/lib/setupPaymentService.ts | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index db3e4bf8cebc2c..4c1917f524f448 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -183,7 +183,7 @@ export async function getConnectedApps({ // undefined it means that app don't require app/setup/page let isSetupAlready = undefined; if (credential && app.categories.includes("payment")) { - isSetupAlready = await setupPaymentService(); + isSetupAlready = await setupPaymentService({ app, credential }); } let dependencyData: TDependencyData = []; diff --git a/packages/lib/setupPaymentService.ts b/packages/lib/setupPaymentService.ts index b8fd2c5a392b7b..f519654a361bbc 100644 --- a/packages/lib/setupPaymentService.ts +++ b/packages/lib/setupPaymentService.ts @@ -1,8 +1,18 @@ -import paymentLoaders, { type PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; +import type getApps from "@calcom/app-store/utils"; +import type { CredentialDataWithTeamName } from "@calcom/app-store/utils"; import type { PaymentApp } from "@calcom/types/PaymentService"; -export const setupPaymentService = async () => { - console.log("Checking payment setup for app", app.name); +type EnabledApp = ReturnType[number] & { enabled: boolean }; + +export const setupPaymentService = async ({ + app, + credential, +}: { + app: Omit; + credential: CredentialDataWithTeamName; +}) => { // @ts-expect-error FIXME const paymentApp = (await paymentLoaders[app.dirName as PaymentLoaderKey]?.()) as PaymentApp | null; if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { From fb71c0725a99f5ea2baeba62bdc15e7ebcca30a5 Mon Sep 17 00:00:00 2001 From: zhyd1997 Date: Sat, 23 Aug 2025 22:10:30 +0800 Subject: [PATCH 22/22] refactor: update `setupPaymentService` to use `PreparedApp` type for app parameter --- packages/app-store/_utils/prepareAppsWithCredentials.ts | 8 +++++++- packages/lib/setupPaymentService.ts | 6 ++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/app-store/_utils/prepareAppsWithCredentials.ts b/packages/app-store/_utils/prepareAppsWithCredentials.ts index ea10d9f9cb03c9..9198bffea6b8fd 100644 --- a/packages/app-store/_utils/prepareAppsWithCredentials.ts +++ b/packages/app-store/_utils/prepareAppsWithCredentials.ts @@ -19,6 +19,12 @@ export type CredentialDataWithTeamName = CredentialForCalendarService & { } | null; }; +export type PreparedApp = App & { + credential: CredentialDataWithTeamName; + credentials: CredentialDataWithTeamName[]; + locationOption: LocationOption | null; +}; + export const prepareAppsWithCredentials = ( apps: App[], credentials: CredentialDataWithTeamName[], @@ -80,5 +86,5 @@ export const prepareAppsWithCredentials = ( }); return reducedArray; - }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); + }, [] as PreparedApp[]); }; diff --git a/packages/lib/setupPaymentService.ts b/packages/lib/setupPaymentService.ts index f519654a361bbc..de4b3fbe4c4a71 100644 --- a/packages/lib/setupPaymentService.ts +++ b/packages/lib/setupPaymentService.ts @@ -1,16 +1,14 @@ import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders"; import type { PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders"; -import type getApps from "@calcom/app-store/utils"; +import type { PreparedApp } from "@calcom/app-store/_utils/prepareAppsWithCredentials"; import type { CredentialDataWithTeamName } from "@calcom/app-store/utils"; import type { PaymentApp } from "@calcom/types/PaymentService"; -type EnabledApp = ReturnType[number] & { enabled: boolean }; - export const setupPaymentService = async ({ app, credential, }: { - app: Omit; + app: Omit; credential: CredentialDataWithTeamName; }) => { // @ts-expect-error FIXME