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:
authored and GitHub committed 2023-12-20 13:29:23 -05:00
1 parent 68d40cabbe
commit d07e86e4f3
15 files changed
+273 -53

No files matched your search

+8 -2
View File
@@ -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={
+2
View File
@@ -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({