mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* 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]>
262 lines
7.8 KiB
TypeScript
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>
|
|
);
|
|
}
|