diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx index 6c96d701cd..6fa09878af 100644 --- a/apps/web/components/eventtype/EventAppsTab.tsx +++ b/apps/web/components/eventtype/EventAppsTab.tsx @@ -47,10 +47,17 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { }; }; - const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => { + const eventTypeFormMetadata = methods.getValues("metadata"); + + const getAppDataSetter = ( + appId: EventTypeAppsList, + appCategories: string[], + credentialId?: number + ): SetAppData => { return function (key, value) { // Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render) const allAppsDataFromForm = methods.getValues("metadata")?.apps || {}; + const appData = allAppsDataFromForm[appId]; setAllAppsData({ ...allAppsDataFromForm, @@ -58,6 +65,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { ...appData, [key]: value, credentialId, + appCategories, }, }); }; @@ -77,10 +85,15 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { appCards.push( ); @@ -91,7 +104,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { appCards.push( { }, }} eventType={eventType} + eventTypeFormMetadata={eventTypeFormMetadata} {...shouldLockDisableProps("apps")} /> ); @@ -148,10 +162,15 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { return ( ); @@ -179,10 +198,11 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { {notInstalledApps?.map((app) => ( ))} diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 5d8069c17e..38baf14feb 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps"; import { getEventLocationType } from "@calcom/app-store/locations"; import { validateCustomEventName } from "@calcom/core/event"; import type { EventLocationType } from "@calcom/core/location"; @@ -484,6 +485,11 @@ const EventTypePage = (props: EventTypeSetupProps) => { } } + // Prevent two payment apps to be enabled + // Ok to cast type here because this metadata will be updated as the event type metadata + if (checkForMultiplePaymentApps(metadata as z.infer)) + throw new Error(t("event_setup_multiple_payment_apps_error")); + if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) { throw new Error(t("seats_and_no_show_fee_error")); } @@ -584,6 +590,12 @@ const EventTypePage = (props: EventTypeSetupProps) => { } } } + + // Prevent two payment apps to be enabled + // Ok to cast type here because this metadata will be updated as the event type metadata + if (checkForMultiplePaymentApps(metadata as z.infer)) + throw new Error(t("event_setup_multiple_payment_apps_error")); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { availability, ...rest } = input; updateMutation.mutate({ diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 3414a03724..c7e4462fa7 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -508,8 +508,8 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { }); }, getPaymentCredential: async () => getPaymentCredential(store.page), - setupEventWithPrice: async (eventType: Pick) => - setupEventWithPrice(eventType, store.page), + setupEventWithPrice: async (eventType: Pick, slug: string) => + setupEventWithPrice(eventType, slug, store.page), bookAndPayEvent: async (eventType: Pick) => bookAndPayEvent(user, eventType, store.page), makePaymentUsingStripe: async () => makePaymentUsingStripe(store.page), @@ -693,9 +693,9 @@ export async function apiLogin( }); } -export async function setupEventWithPrice(eventType: Pick, page: Page) { +export async function setupEventWithPrice(eventType: Pick, slug: string, page: Page) { await page.goto(`/event-types/${eventType?.id}?tabName=apps`); - await page.locator("[data-testid='app-switch']").first().click(); + await page.locator(`[data-testid='${slug}-app-switch']`).first().click(); await page.getByPlaceholder("Price").fill("100"); await page.getByTestId("update-eventtype").click(); } diff --git a/apps/web/playwright/integrations-stripe.e2e.ts b/apps/web/playwright/integrations-stripe.e2e.ts index afa55a246c..1d76db2699 100644 --- a/apps/web/playwright/integrations-stripe.e2e.ts +++ b/apps/web/playwright/integrations-stripe.e2e.ts @@ -46,7 +46,7 @@ test.describe("Stripe integration", () => { await user.getPaymentCredential(); const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType; - await user.setupEventWithPrice(eventType); + await user.setupEventWithPrice(eventType, "stripe"); // Need to wait for the DB to be updated with the metadata await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200); @@ -104,7 +104,7 @@ test.describe("Stripe integration", () => { page.click('[id="skip-account-app"]'), ]); - await owner.setupEventWithPrice(teamEvent); + await owner.setupEventWithPrice(teamEvent, "stripe"); // Need to wait for the DB to be updated with the metadata await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200); @@ -134,7 +134,7 @@ test.describe("Stripe integration", () => { await page.goto("/apps/installed"); await user.getPaymentCredential(); - await user.setupEventWithPrice(eventType); + await user.setupEventWithPrice(eventType, "stripe"); await user.bookAndPayEvent(eventType); // success await expect(page.locator("[data-testid=success-page]")).toBeVisible(); @@ -147,7 +147,7 @@ test.describe("Stripe integration", () => { await page.goto("/apps/installed"); await user.getPaymentCredential(); - await user.setupEventWithPrice(eventType); + await user.setupEventWithPrice(eventType, "stripe"); // booking process without payment await page.goto(`${user.username}/${eventType?.slug}`); @@ -171,7 +171,7 @@ test.describe("Stripe integration", () => { await page.goto("/apps/installed"); await user.getPaymentCredential(); - await user.setupEventWithPrice(eventType); + await user.setupEventWithPrice(eventType, "stripe"); await user.bookAndPayEvent(eventType); // Rescheduling the event @@ -194,7 +194,7 @@ test.describe("Stripe integration", () => { await page.goto("/apps/installed"); await user.getPaymentCredential(); - await user.setupEventWithPrice(eventType); + await user.setupEventWithPrice(eventType, "stripe"); await user.bookAndPayEvent(eventType); await page.click('[data-testid="cancel"]'); @@ -214,7 +214,7 @@ test.describe("Stripe integration", () => { await page.goto("/apps/installed"); await user.getPaymentCredential(); - await user.setupEventWithPrice(eventType); + await user.setupEventWithPrice(eventType, "stripe"); await user.bookAndPayEvent(eventType); await user.confirmPendingPayment(); }); @@ -264,7 +264,7 @@ test.describe("Stripe integration", () => { await page.locator("#event-type-form").getByRole("switch").click(); // Set price - await page.getByTestId("price-input-stripe").fill("200"); + await page.getByTestId("stripe-price-input").fill("200"); // Select currency in dropdown await page.getByTestId("stripe-currency-select").click(); diff --git a/apps/web/playwright/payment-apps.e2e.ts b/apps/web/playwright/payment-apps.e2e.ts index 77bf674d92..998fa50888 100644 --- a/apps/web/playwright/payment-apps.e2e.ts +++ b/apps/web/playwright/payment-apps.e2e.ts @@ -80,8 +80,8 @@ test.describe("Payment app", () => { await page.getByTestId("stripe-currency-select").click(); await page.getByTestId("select-option-usd").click(); - await page.getByTestId("price-input-stripe").click(); - await page.getByTestId("price-input-stripe").fill("350"); + await page.getByTestId("stripe-price-input").click(); + await page.getByTestId("stripe-price-input").fill("350"); await page.getByTestId("update-eventtype").click(); await page.goto(`${user.username}/${paymentEvent.slug}`); @@ -234,4 +234,128 @@ test.describe("Payment app", () => { await page.getByLabel("Tracking ID").fill("demo"); await page.getByTestId("update-eventtype").click(); }); + + test("Should only be allowed to enable one payment app", async ({ page, users }) => { + const user = await users.create(); + await user.apiLogin(); + const paymentEvent = user.eventTypes.find((item) => item.slug === "paid"); + if (!paymentEvent) { + throw new Error("No payment event found"); + } + await prisma.credential.createMany({ + data: [ + { + type: "paypal_payment", + userId: user.id, + key: { + client_id: "randomString", + secret_key: "randomString", + webhook_id: "randomString", + }, + }, + { + type: "stripe_payment", + userId: user.id, + key: { + scope: "read_write", + livemode: false, + token_type: "bearer", + access_token: "sk_test_randomString", + refresh_token: "rt_randomString", + stripe_user_id: "acct_randomString", + default_currency: "usd", + stripe_publishable_key: "pk_test_randomString", + }, + }, + ], + }); + + await page.goto(`event-types/${paymentEvent.id}?tabName=apps`); + + await page.locator("[data-testid='paypal-app-switch']").click(); + await page.locator("[data-testid='stripe-app-switch']").isDisabled(); + }); + + test("when more than one payment app is installed the price should be updated when changing settings", async ({ + page, + users, + }) => { + const user = await users.create(); + await user.apiLogin(); + const paymentEvent = user.eventTypes.find((item) => item.slug === "paid"); + if (!paymentEvent) { + throw new Error("No payment event found"); + } + + await prisma.credential.createMany({ + data: [ + { + type: "paypal_payment", + userId: user.id, + key: { + client_id: "randomString", + secret_key: "randomString", + webhook_id: "randomString", + }, + }, + { + type: "stripe_payment", + userId: user.id, + key: { + scope: "read_write", + livemode: false, + token_type: "bearer", + access_token: "sk_test_randomString", + refresh_token: "rt_randomString", + stripe_user_id: "acct_randomString", + default_currency: "usd", + stripe_publishable_key: "pk_test_randomString", + }, + }, + ], + }); + + await page.goto(`event-types/${paymentEvent.id}?tabName=apps`); + + await page.locator("[data-testid='paypal-app-switch']").click(); + await page.locator("[data-testid='paypal-price-input']").fill("100"); + await page.locator("[data-testid='paypal-currency-select']").first().click(); + await page.locator("#react-select-2-option-13").click(); + // await page.locator(".mb-1 > .bg-default > div > div:nth-child(2)").first().click(); + // await page.getByText("$MXNCurrencyMexican pesoPayment option").click(); + await page.locator("[data-testid='update-eventtype']").click(); + + // Need to wait for the DB to be updated + await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200); + + const paypalPrice = await prisma.eventType.findFirst({ + where: { + id: paymentEvent.id, + }, + select: { + price: true, + }, + }); + + expect(paypalPrice?.price).toEqual(10000); + + await page.locator("[data-testid='paypal-app-switch']").click(); + await page.locator("[data-testid='stripe-app-switch']").click(); + await page.locator("[data-testid='stripe-price-input']").fill("200"); + await page.locator("[data-testid='update-eventtype']").click(); + + // Need to wait for the DB to be updated + await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200); + + const stripePrice = await prisma.eventType.findFirst({ + where: { + id: paymentEvent.id, + }, + select: { + price: true, + }, + }); + + expect(stripePrice?.price).toEqual(20000); + }); }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index e553cc233f..324e39cf26 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2158,6 +2158,7 @@ "manage_availability_schedules":"Manage availability schedules", "lock_timezone_toggle_on_booking_page": "Lock timezone on booking page", "description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.", + "event_setup_multiple_payment_apps_error": "You can only have one payment app enabled per event type.", "number_in_international_format": "Please enter number in international format.", "install_calendar":"Install Calendar", "branded_subdomain": "Branded Subdomain", @@ -2175,6 +2176,7 @@ "enterprise_description": "Upgrade to Enterprise to create your Organization", "create_your_org": "Create your Organization", "create_your_org_description": "Upgrade to Enterprise and receive a subdomain, unified billing, Insights, extensive whitelabeling and more", + "other_payment_app_enabled": "You can only enable one payment app per event type", "admin_delete_organization_description": "
  • Teams that are member of this organization will also be deleted along with their event-types
  • Users that were part of the organization will not be deleted and their event-types will also remain intact.
  • Usernames would be changed to allow them to exist outside the organization
", "admin_delete_organization_title": "Delete organization?", "published": "Published", diff --git a/packages/app-store/_components/AppCard.tsx b/packages/app-store/_components/AppCard.tsx index e946aab684..a641aa454b 100644 --- a/packages/app-store/_components/AppCard.tsx +++ b/packages/app-store/_components/AppCard.tsx @@ -19,6 +19,8 @@ export default function AppCard({ children, returnTo, teamId, + disableSwitch, + switchTooltip, }: { app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner }; description?: React.ReactNode; @@ -28,10 +30,12 @@ export default function AppCard({ returnTo?: string; teamId?: number; LockedIcon?: React.ReactNode; + disableSwitch?: boolean; + switchTooltip?: string; }) { const { t } = useTranslation(); const [animationRef] = useAutoAnimate(); - const { setAppData, LockedIcon, disabled } = useAppContextWithSchema(); + const { setAppData, LockedIcon, disabled: managedDisabled } = useAppContextWithSchema(); return (
{ if (switchOnClick) { switchOnClick(enabled); @@ -100,6 +104,8 @@ export default function AppCard({ }} checked={switchChecked} LockedIcon={LockedIcon} + data-testId={`${app.slug}-app-switch`} + tooltip={switchTooltip} />
) : ( diff --git a/packages/app-store/_components/EventTypeAppCardInterface.tsx b/packages/app-store/_components/EventTypeAppCardInterface.tsx index 8f34ce9e11..c02ec480ba 100644 --- a/packages/app-store/_components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/_components/EventTypeAppCardInterface.tsx @@ -1,6 +1,9 @@ +import type z from "zod"; + import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; import EventTypeAppContext from "@calcom/app-store/EventTypeAppContext"; import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated"; +import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { RouterOutputs } from "@calcom/trpc/react"; import { ErrorBoundary } from "@calcom/ui"; @@ -14,6 +17,7 @@ export const EventTypeAppCard = (props: { setAppData: SetAppData; // For event type apps, get these props from shouldLockDisableProps LockedIcon?: JSX.Element | false; + eventTypeFormMetadata: z.infer; disabled?: boolean; }) => { const { app, getAppData, setAppData, LockedIcon, disabled } = props; diff --git a/packages/app-store/_utils/payments/checkForMultiplePaymentApps.ts b/packages/app-store/_utils/payments/checkForMultiplePaymentApps.ts new file mode 100644 index 0000000000..e1186b4a1e --- /dev/null +++ b/packages/app-store/_utils/payments/checkForMultiplePaymentApps.ts @@ -0,0 +1,34 @@ +import type z from "zod"; + +import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +import type { appDataSchemas } from "../../apps.schemas.generated"; + +/** + * + * @param metadata The event type metadata + * @param inclusive Determines if multiple includes the case of 1 + * @returns boolean + */ +const checkForMultiplePaymentApps = ( + metadata: z.infer, + inclusive = false +) => { + let enabledPaymentApps = 0; + for (const appKey in metadata?.apps) { + const app = metadata?.apps[appKey as keyof typeof appDataSchemas]; + + if ("appCategories" in app) { + const isPaymentApp = app.appCategories.includes("payment"); + if (isPaymentApp && app.enabled) { + enabledPaymentApps++; + } + } else if ("price" in app && app.enabled) { + enabledPaymentApps++; + } + } + + return inclusive ? enabledPaymentApps >= 1 : enabledPaymentApps > 1; +}; + +export default checkForMultiplePaymentApps; diff --git a/packages/app-store/alby/components/EventTypeAppCardInterface.tsx b/packages/app-store/alby/components/EventTypeAppCardInterface.tsx index 0eb71ba136..f6bf717dd6 100644 --- a/packages/app-store/alby/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/alby/components/EventTypeAppCardInterface.tsx @@ -10,12 +10,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, Select, TextField } from "@calcom/ui"; import { SatSymbol } from "@calcom/ui/components/icon/SatSymbol"; +import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps"; import type { appDataSchema } from "../zod"; import { PaypalPaymentOptions as paymentOptions } from "../zod"; type Option = { value: string; label: string }; -const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ + app, + eventType, + eventTypeFormMetadata, +}) { const { asPath } = useRouter(); const { getAppData, setAppData } = useAppContextWithSchema(); const price = getAppData("price"); @@ -32,6 +37,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); const { t } = useLocale(); const recurringEventDefined = eventType.recurringEvent?.count !== undefined; + const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); + + const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; // make sure a currency is selected useEffect(() => { @@ -48,7 +56,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ switchOnClick={(enabled) => { setRequirePayment(enabled); }} - description={<>Add bitcoin lightning payments to your events}> + description={<>Add bitcoin lightning payments to your events} + disableSwitch={shouldDisableSwitch} + switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}> <> {recurringEventDefined ? ( diff --git a/packages/app-store/eventTypeAppCardZod.ts b/packages/app-store/eventTypeAppCardZod.ts index 28c3878eb1..dd1630bc3a 100644 --- a/packages/app-store/eventTypeAppCardZod.ts +++ b/packages/app-store/eventTypeAppCardZod.ts @@ -4,6 +4,7 @@ import { z } from "zod"; export const eventTypeAppCardZod = z.object({ enabled: z.boolean().optional(), credentialId: z.number().optional(), + appCategories: z.array(z.string()).optional(), }); export const appKeysSchema = z.object({}); diff --git a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx index db6ba04755..008d875cb6 100644 --- a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx @@ -13,12 +13,17 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, Select, TextField } from "@calcom/ui"; +import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps"; import type { appDataSchema } from "../zod"; import { PaypalPaymentOptions as paymentOptions } from "../zod"; type Option = { value: string; label: string }; -const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ + app, + eventType, + eventTypeFormMetadata, +}) { const { asPath } = useRouter(); const { getAppData, setAppData } = useAppContextWithSchema(); const price = getAppData("price"); @@ -38,6 +43,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); const { t } = useLocale(); const recurringEventDefined = eventType.recurringEvent?.count !== undefined; + const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); + + const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; useEffect(() => { if (requirePayment) { @@ -58,7 +66,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ switchOnClick={(enabled) => { setRequirePayment(enabled); }} - description={<>Add Paypal payment to your events}> + description={<>Add Paypal payment to your events} + disableSwitch={shouldDisableSwitch} + switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}> <> {recurringEventDefined ? ( @@ -77,6 +87,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ required className="block w-full rounded-sm pl-2 text-sm" placeholder="Price" + data-testid="paypal-price-input" onChange={(e) => { setAppData("price", Number(e.target.value) * 100); if (selectedCurrency) { diff --git a/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx b/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx index 04498af048..886f626913 100644 --- a/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx @@ -8,6 +8,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, Select, TextField } from "@calcom/ui"; +import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps"; import { paymentOptions } from "../lib/constants"; import { convertToSmallestCurrencyUnit, @@ -18,7 +19,11 @@ import type { appDataSchema } from "../zod"; type Option = { value: string; label: string }; -const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ + app, + eventType, + eventTypeFormMetadata, +}) { const pathname = usePathname(); const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const price = getAppData("price"); @@ -32,6 +37,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ const paymentOption = getAppData("paymentOption"); const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value); const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); + const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); + + const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; const { t } = useLocale(); const recurringEventDefined = eventType.recurringEvent?.count !== undefined; @@ -66,7 +74,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ switchOnClick={(enabled) => { setRequirePayment(enabled); }} - teamId={eventType.team?.id || undefined}> + teamId={eventType.team?.id || undefined} + disableSwitch={shouldDisableSwitch} + switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}> <> {recurringEventDefined && ( @@ -75,7 +85,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ <>
; }; export type EventTypeAppCardComponent = React.FC; diff --git a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts index 1f5fb9e13b..96bf895607 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts @@ -1,6 +1,7 @@ import { Prisma } from "@prisma/client"; import type { NextApiResponse, GetServerSidePropsContext } from "next"; +import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated"; import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes"; import { validateIntervalLimitOrder } from "@calcom/lib"; import logger from "@calcom/lib/logger"; @@ -240,31 +241,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { } } - /** - * Since you can have multiple payment apps we will honor the first one to save in eventType - * but the real detail will be inside app metadata, so with this you can have different prices in different apps - * So the price and currency inside eventType will be deprecated soon or just keep as reference. - */ - if ( - input.metadata?.apps?.alby?.price || - input?.metadata?.apps?.paypal?.price || - input?.metadata?.apps?.stripe?.price - ) { - data.price = - input.metadata?.apps?.alby?.price || - input.metadata.apps.paypal?.price || - input.metadata.apps.stripe?.price; - } - - if ( - input.metadata?.apps?.alby?.currency || - input?.metadata?.apps?.paypal?.currency || - input?.metadata?.apps?.stripe?.currency - ) { - data.currency = - input.metadata?.apps?.alby?.currency || - input.metadata.apps.paypal?.currency || - input.metadata.apps.stripe?.currency; + for (const appKey in input.metadata?.apps) { + const app = input.metadata?.apps[appKey as keyof typeof appDataSchemas]; + // There should only be one enabled payment app in the metadata + if (app.enabled && app.price && app.currency) { + data.price = app.price; + data.currency = app.currency; + break; + } } const connectedLink = await ctx.prisma.hashedLink.findFirst({