Files
d6546c3107 feat: upgrade tailwind v4 (#24598)
* chore: refactor config files to prevent migration tool errors

* refactor: upgrade with the tailwind migration tool

* chore: restore pre-commit command + mc

* refactor(wip): update dependencies and migrate to Tailwind CSS v4 (mainly web)

* chore: resolve Tailwind v4 migration conflicts from merging main

* chore: remove unused Tailwind packages from config and update dependencies for v4 migration

* chore: uncomment Tailwind CSS utility classes in globals.css

* fix: resolve token conflicts between calcom and coss ui

* fix: textarea scrollbar

* Fix CUI-16

* fix: added @tailwindcss/forms plugin and cleaned up CSS classes in various components to remove unnecessary dark mode styles

* fix: selects and inputs of different sizes

* fix: remove unnecessary leading-20 class from modal titles in various components

* fix: update Checkbox component styles to remove unnecessary border on checked state

* fix: clean up styles in RequiresConfirmationController, Checkbox, and Radio components to enhance consistency

* fix: update button and filter component styles to remove unnecessary rounded classes for consistency

* fix: calendar

* fix: update KBarSearch

* fix: refine styles in Empty and Checkbox components for improved consistency

* Fix focus state email input

* fix: update button hover and active states to use 'not-disabled' instead of 'enabled'

* fix: line-height issues

* fix: update class name for muted background in BookingListItem component

* fix: sidebar spacing

* chore: update class names to use new Tailwind CSS color utilities

* fix embed

* chore: upgrade Tailwind CSS to version 4.1.16 and update related dependencies

* Map css variables and add a playground test for heavy css customization

* suggestion for coss-ui

* refactor: update CSS variable usage and clean up styles

- Replace instances of `--cal-brand-color` with `--cal-brand` in embed-related HTML files.
- Remove the now-unnecessary `addAppCssVars` function from the embed core.
- Import theme tokens in the embed core styles for better consistency.
- Clean up whitespace and formatting in CSS files for improved readability.
- Add a comment in `tokens.css` regarding its usage in both embed and webapp contexts.

* Handle within tokens.css instead of fixing coss-ui

* Remove initial, not needed. Also, remove tailwind.config.js as tailwidn scans the html automaically

* fix: examples app breaking

* fix: modal not resizing correctly

* feat: upgrade atoms to tailwind v4

* fix: atoms build breaking

* fix: atoms build breaking

* chore: upgrate examples/base to tailwind 4

* chore: update globals.css

* fix: add missing scheduler css variables

* fix: PlatformAdditionalCalendarSelector

* chore: update global styles

* chore: update tailwindcss and postcss dependencies to stable versions

* chore: remove unneeded class

* fix: dialog and toast animation

* fix: replace flex-shrink-0 with shrink-0 for consistent styling in various components

* fix: dialog modal for Apple connect

* add margin in SaveFilterSegmentButton

* Fix radix button nested states

* add cursor pointer to buttons but keep dsabled state

* Fix commandK selectors and adds cursor pointer

* Fix teams filter

* fix - round checkboxes

* fix filter checkbox

* fix select indicator's margin

* command group font size

* style: fix badge and tooltip radius

* chore: remove unneeded files

* Delete PR_REVIEW_MANAGED_EVENT_REASSIGNMENT.md

* remove ui-playground leftover

* fix: add missing react phone input styles in atoms

* Delete managed-event-reassignment-flow-and-architecture.mermaid

* fix: inter font not loading

* Add theme to skeleton container so that it can support dark mode

* fix: create custom stack-y-* utilities post tw4 upgrade

* fix: typo

* fix: atoms stack class + remove unused css file

* fix default radius valiue

* fix space-y in embed

* fix skeleton background

* Hardcode radius values to match production

* fix border in embed

* add missing externalThemeClass

* feat: create a custom stack-y-* utility

* fix: add stack utility to atom global css

* fix: Skeleton loader class modalbox

* Add stack-y utility in embed

* fix: add missing stack utilities in atoms globals.css

* update yarn.lock

* add popover portla

* update

---------

Co-authored-by: Sean Brydon <[email protected]>
Co-authored-by: Hariom Balhara <[email protected]>
Co-authored-by: Ryukemeister <[email protected]>
Co-authored-by: cal.com <[email protected]>
Co-authored-by: Eunjae Lee <[email protected]>
Co-authored-by: Anik Dhabal Babu <[email protected]>
2025-11-25 17:32:28 -03:00

262 lines
7.8 KiB
TypeScript

"use client";
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
import type { StripeElementLocale, StripeElements, StripePaymentElementOptions } from "@stripe/stripe-js";
import { useRouter } from "next/navigation";
import type { SyntheticEvent } from "react";
import { useEffect, useState } from "react";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventType, Payment } from "@calcom/prisma/client";
import type { PaymentOption } from "@calcom/prisma/enums";
import { Button } from "@calcom/ui/components/button";
import { CheckboxField } from "@calcom/ui/components/form";
import type { PaymentPageProps } from "../pages/payment";
export type Props = {
payment: Omit<Payment, "id" | "fee" | "success" | "refunded" | "externalId" | "data"> & {
data: Record<string, unknown>;
};
eventType: {
id: number;
successRedirectUrl: EventType["successRedirectUrl"];
forwardParamsSuccessRedirect: EventType["forwardParamsSuccessRedirect"];
};
user: {
username: string | null;
};
location?: string | null;
clientSecret: string;
booking: PaymentPageProps["booking"];
};
export type States =
| {
status: "idle";
}
| {
status: "processing";
}
| {
status: "error";
error: Error;
}
| {
status: "ok";
};
export const PaymentFormComponent = (
props: Props & {
onSubmit: (ev: SyntheticEvent) => void;
onCancel: () => void;
onPaymentElementChange: () => void;
elements: StripeElements | null;
paymentOption: PaymentOption | null;
state: States;
}
) => {
const { t, i18n } = useLocale();
const { paymentOption, elements, state, onPaymentElementChange } = props;
const [isCanceling, setIsCanceling] = useState<boolean>(false);
const [holdAcknowledged, setHoldAcknowledged] = useState<boolean>(paymentOption === "HOLD" ? false : true);
const disableButtons = isCanceling || !holdAcknowledged || ["processing", "error"].includes(state.status);
const paymentElementOptions = {
layout: "accordion",
} as StripePaymentElementOptions;
useEffect(() => {
elements?.update({ locale: i18n.language as StripeElementLocale });
}, [elements, i18n.language]);
return (
<form id="payment-form" className="bg-subtle mt-4 rounded-md p-6" onSubmit={props.onSubmit}>
<div>
<PaymentElement options={paymentElementOptions} onChange={(_) => onPaymentElementChange()} />
</div>
{paymentOption === "HOLD" && (
<div className="bg-cal-info mb-5 mt-2 rounded-md p-3">
<CheckboxField
description={t("acknowledge_booking_no_show_fee", {
amount: props.payment.amount / 100,
formatParams: { amount: { currency: props.payment.currency } },
})}
onChange={(e) => setHoldAcknowledged(e.target.checked)}
descriptionClassName="text-info font-semibold"
/>
</div>
)}
<div className="mt-2 flex justify-end space-x-2">
<Button
color="minimal"
disabled={disableButtons}
id="cancel"
type="button"
loading={isCanceling}
onClick={() => {
setIsCanceling(true);
props.onCancel();
}}>
<span id="button-text">{t("cancel")}</span>
</Button>
<Button
type="submit"
disabled={disableButtons}
loading={state.status === "processing"}
id="submit"
color="secondary">
<span id="button-text">
{state.status === "processing" ? (
<div className="spinner" id="spinner" />
) : paymentOption === "HOLD" ? (
t("submit_card")
) : (
t("pay_now")
)}
</span>
</Button>
</div>
{state.status === "error" && (
<div className="mt-4 text-center text-red-900 dark:text-gray-300" role="alert">
{state.error.message}
</div>
)}
</form>
);
};
const PaymentForm = (props: Props) => {
const {
user: { username },
} = props;
const { t } = useLocale();
const router = useRouter();
const searchParams = useCompatSearchParams();
const [state, setState] = useState<States>({ status: "idle" });
const stripe = useStripe();
const elements = useElements();
const paymentOption = props.payment.paymentOption;
const bookingSuccessRedirect = useBookingSuccessRedirect();
const handleSubmit = async (ev: SyntheticEvent) => {
ev.preventDefault();
if (!stripe || !elements || searchParams === null) {
return;
}
if (!stripe || !elements) {
return;
}
setState({ status: "processing" });
let payload;
const params: {
uid: string;
email: string | null;
location?: string;
payment_intent?: string;
payment_intent_client_secret?: string;
redirect_status?: string;
} = {
uid: props.booking.uid,
email: searchParams?.get("email"),
};
if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) {
payload = await stripe.confirmSetup({
elements,
redirect: "if_required",
});
if (payload.setupIntent) {
params.payment_intent = payload.setupIntent.id;
params.payment_intent_client_secret = payload.setupIntent.client_secret || undefined;
params.redirect_status = payload.setupIntent.status;
}
} else if (paymentOption === "ON_BOOKING") {
payload = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${WEBAPP_URL}/booking/${params.uid}`,
},
redirect: "if_required",
});
if (payload.paymentIntent) {
params.payment_intent = payload.paymentIntent.id;
params.payment_intent_client_secret = payload.paymentIntent.client_secret || undefined;
params.redirect_status = payload.paymentIntent.status;
}
}
if (payload?.error) {
setState({
status: "error",
error: new Error(`Payment failed: ${payload.error.message}`),
});
} else {
if (props.location) {
if (props.location.includes("integration")) {
params.location = t("web_conferencing_details_to_follow");
} else {
params.location = props.location;
}
}
return bookingSuccessRedirect({
successRedirectUrl: props.eventType.successRedirectUrl,
query: params,
booking: props.booking,
forwardParamsSuccessRedirect: props.eventType.forwardParamsSuccessRedirect,
});
}
};
return (
<PaymentFormComponent
{...props}
elements={elements}
paymentOption={paymentOption}
state={state}
onSubmit={handleSubmit}
onCancel={() => {
if (username) {
return router.push(`/${username}`);
}
return router.back();
}}
onPaymentElementChange={() => {
setState({ status: "idle" });
}}
/>
);
};
export default function PaymentComponent(props: Props) {
const stripePromise = getStripe(props.payment.data.stripe_publishable_key as any);
const [theme, setTheme] = useState<"stripe" | "night">("stripe");
useEffect(() => {
if (document.documentElement.classList.contains("dark")) {
setTheme("night");
}
}, []);
return (
<Elements
stripe={stripePromise}
options={{
clientSecret: props.clientSecret,
appearance: {
theme,
},
}}>
<PaymentForm {...props} />
</Elements>
);
}