fix: Update Event Type Pricing For Multiple Installed Payment Apps (#12272)
* Prevent two payment apps from being enabled * Find the enabled payment app to update the event type * Add string * Add tests * Type fix * Abstract check for multiple payment app logic * Type check * Address feedback * chore: Enable One Payment App Per Event Type (#12414) Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay <morgan@cal.com> * Fix bug * Fix test * Clean up * Fix test * Fix test --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
15 files changed
+273
-53
No files matched your search
@@ -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<HTMLDivElement>();
|
||||
const { setAppData, LockedIcon, disabled } = useAppContextWithSchema();
|
||||
const { setAppData, LockedIcon, disabled: managedDisabled } = useAppContextWithSchema();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -91,7 +95,7 @@ export default function AppCard({
|
||||
<div className="ml-auto flex items-center">
|
||||
<Switch
|
||||
data-testid="app-switch"
|
||||
disabled={!app.enabled || disabled}
|
||||
disabled={!app.enabled || managedDisabled || disableSwitch}
|
||||
onCheckedChange={(enabled) => {
|
||||
if (switchOnClick) {
|
||||
switchOnClick(enabled);
|
||||
@@ -100,6 +104,8 @@ export default function AppCard({
|
||||
}}
|
||||
checked={switchChecked}
|
||||
LockedIcon={LockedIcon}
|
||||
data-testId={`${app.slug}-app-switch`}
|
||||
tooltip={switchTooltip}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -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<typeof EventTypeMetaDataSchema>;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { app, getAppData, setAppData, LockedIcon, disabled } = props;
|
||||
|
||||
@@ -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<typeof EventTypeMetaDataSchema>,
|
||||
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;
|
||||
@@ -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<typeof appDataSchema>();
|
||||
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 ? (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
|
||||
@@ -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({});
|
||||
@@ -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<typeof appDataSchema>();
|
||||
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 ? (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<typeof appDataSchema>();
|
||||
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 && (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
@@ -75,7 +85,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
<>
|
||||
<div className="mt-4 block items-center justify-start sm:flex sm:space-x-2">
|
||||
<TextField
|
||||
data-testid="price-input-stripe"
|
||||
data-testid="stripe-price-input"
|
||||
label={t("price")}
|
||||
className="h-[38px]"
|
||||
addOnLeading={
|
||||
|
||||
Vendored
+2
@@ -1,6 +1,7 @@
|
||||
import type React from "react";
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { EventTypeFormMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { ButtonProps } from "@calcom/ui";
|
||||
|
||||
@@ -51,5 +52,6 @@ export type EventTypeAppCardComponentProps = {
|
||||
app: EventTypeAppCardApp;
|
||||
disabled?: boolean;
|
||||
LockedIcon?: JSX.Element | false;
|
||||
eventTypeFormMetadata?: z.infer<typeof EventTypeFormMetadataSchema>;
|
||||
};
|
||||
export type EventTypeAppCardComponent = React.FC<EventTypeAppCardComponentProps>;
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user