feat: payment card atom (#17203)

* init stripe payment card atom

* cleanup

* update platform libraries

* fix import paths

* add function to get user payment info

* fixup

* resolve merge conflicts

* fixup fixup

* resolve merge conflicts

* fixup

* update payments page

* add props for event payment success and failure

* update props and fixup

* bump platform libraries

* fixup! bump platform libraries

* fixup! fixup! bump platform libraries

* resolve merge conflicts

* fix: add isActive to paymentsTab

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
Rajiv Sahal
2024-10-31 11:21:13 +02:00
committed by GitHub
co-authored by Morgan Morgan Vernay
parent a5806cb100
commit fee3b73bda
15 changed files with 602 additions and 129 deletions
@@ -1,5 +1,6 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
@@ -17,6 +18,7 @@ import { Module } from "@nestjs/common";
MembershipsRepository,
CredentialsRepository,
UsersRepository,
AtomsRepository,
UsersService,
SchedulesRepository_2024_06_11,
],
@@ -0,0 +1,17 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { paymentDataSelect } from "@calcom/platform-libraries";
@Injectable()
export class AtomsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getRawPayment(uid: string) {
return await this.dbRead.prisma.payment.findFirst({
where: { uid },
select: paymentDataSelect,
});
}
}
@@ -72,6 +72,18 @@ export class AtomsController {
};
}
@Get("payment/:uid")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
async getUserPaymentInfoById(@Param("uid") uid: string): Promise<ApiResponse<unknown>> {
const data = await this.eventTypesService.getUserPaymentInfo(uid);
return {
status: SUCCESS_STATUS,
data,
};
}
@Patch("event-types/:eventTypeId")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
@@ -1,4 +1,5 @@
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
@@ -17,6 +18,7 @@ import {
getEnabledAppsFromCredentials,
getAppFromSlug,
MembershipRole,
EventTypeMetaDataSchema,
} from "@calcom/platform-libraries";
import type {
App,
@@ -27,6 +29,7 @@ import type {
TDependencyData,
CredentialPayload,
} from "@calcom/platform-libraries";
import { getClientSecretFromPayment } from "@calcom/platform-libraries";
import { PrismaClient } from "@calcom/prisma/client";
type EnabledAppType = App & {
@@ -40,6 +43,7 @@ export class EventTypesAtomService {
constructor(
private readonly membershipsRepository: MembershipsRepository,
private readonly credentialsRepository: CredentialsRepository,
private readonly atomsRepository: AtomsRepository,
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
private readonly usersService: UsersService,
private readonly dbWrite: PrismaWriteService,
@@ -274,4 +278,43 @@ export class EventTypesAtomService {
);
return apps[0];
}
async getUserPaymentInfo(uid: string) {
const rawPayment = await this.atomsRepository.getRawPayment(uid);
if (!rawPayment) throw new NotFoundException(`Payment with uid ${uid} not found`);
const { data, booking: _booking, ...restPayment } = rawPayment;
const payment = {
...restPayment,
data: data as Record<string, unknown>,
};
if (!_booking) throw new NotFoundException(`Booking with uid ${uid} not found`);
const { startTime, endTime, eventType, ...restBooking } = _booking;
const booking = {
...restBooking,
startTime: startTime.toString(),
endTime: endTime.toString(),
};
if (!eventType) throw new NotFoundException(`Event type with uid ${uid} not found`);
if (eventType.users.length === 0 && !!!eventType.team)
throw new NotFoundException(`No users found or no team present for event type with uid ${uid}`);
const [user] = eventType?.users.length
? eventType.users
: [{ name: null, theme: null, hideBranding: null, username: null }];
const profile = {
name: eventType.team?.name || user?.name || null,
theme: (!eventType.team?.name && user?.theme) || null,
hideBranding: eventType.team?.hideBranding || user?.hideBranding || null,
};
return {
user,
eventType: {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
},
booking,
payment,
clientSecret: getClientSecretFromPayment(payment),
profile,
};
}
}
@@ -1,7 +1,7 @@
import type { Payment } from "@prisma/client";
import type { EventType } from "@prisma/client";
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
import type { StripeElementLocale } from "@stripe/stripe-js";
import type { StripeElementLocale, StripeElements } from "@stripe/stripe-js";
import { useRouter } from "next/navigation";
import type { SyntheticEvent } from "react";
import { useEffect, useState } from "react";
@@ -10,11 +10,12 @@ import getStripe from "@calcom/app-store/stripepayment/lib/client";
import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PaymentOption } from "@calcom/prisma/enums";
import { Button, CheckboxField } from "@calcom/ui";
import type { PaymentPageProps } from "../pages/payment";
type Props = {
export type Props = {
payment: Omit<Payment, "id" | "fee" | "success" | "refunded" | "externalId" | "data"> & {
data: Record<string, unknown>;
};
@@ -31,7 +32,7 @@ type Props = {
booking: PaymentPageProps["booking"];
};
type States =
export type States =
| {
status: "idle";
}
@@ -46,23 +47,94 @@ type States =
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);
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 onChange={() => onPaymentElementChange()} />
</div>
{paymentOption === "HOLD" && (
<div className="bg-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, i18n } = useLocale();
const { t } = useLocale();
const router = useRouter();
const searchParams = useCompatSearchParams();
const [state, setState] = useState<States>({ status: "idle" });
const [isCanceling, setIsCanceling] = useState<boolean>(false);
const stripe = useStripe();
const elements = useElements();
const paymentOption = props.payment.paymentOption;
const [holdAcknowledged, setHoldAcknowledged] = useState<boolean>(paymentOption === "HOLD" ? false : true);
const bookingSuccessRedirect = useBookingSuccessRedirect();
useEffect(() => {
elements?.update({ locale: i18n.language as StripeElementLocale });
}, [elements, i18n.language]);
const handleSubmit = async (ev: SyntheticEvent) => {
ev.preventDefault();
@@ -71,6 +143,10 @@ const PaymentForm = (props: Props) => {
return;
}
if (!stripe || !elements) {
return;
}
setState({ status: "processing" });
let payload;
@@ -130,64 +206,23 @@ const PaymentForm = (props: Props) => {
}
};
const disableButtons = isCanceling || !holdAcknowledged || ["processing", "error"].includes(state.status);
return (
<form id="payment-form" className="bg-subtle mt-4 rounded-md p-6" onSubmit={handleSubmit}>
<div>
<PaymentElement onChange={() => setState({ status: "idle" })} />
</div>
{paymentOption === "HOLD" && (
<div className="bg-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);
if (username) {
return router.push(`/${username}`);
}
return router.back();
}}>
<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>
<PaymentFormComponent
{...props}
elements={elements}
paymentOption={paymentOption}
state={state}
onSubmit={handleSubmit}
onCancel={() => {
if (username) {
return router.push(`/${username}`);
}
return router.back();
}}
onPaymentElementChange={() => {
setState({ status: "idle" });
}}
/>
);
};
@@ -4,6 +4,7 @@ import { z } from "zod";
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { paymentDataSelect } from "@calcom/prisma/selects/payment";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -23,68 +24,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
where: {
uid,
},
select: {
data: true,
success: true,
uid: true,
refunded: true,
bookingId: true,
appId: true,
amount: true,
currency: true,
paymentOption: true,
booking: {
select: {
id: true,
uid: true,
description: true,
title: true,
startTime: true,
endTime: true,
attendees: {
select: {
email: true,
name: true,
},
},
eventTypeId: true,
location: true,
status: true,
rejectionReason: true,
cancellationReason: true,
eventType: {
select: {
id: true,
title: true,
description: true,
length: true,
eventName: true,
requiresConfirmation: true,
userId: true,
metadata: true,
users: {
select: {
name: true,
username: true,
hideBranding: true,
theme: true,
},
},
team: {
select: {
name: true,
hideBranding: true,
},
},
price: true,
currency: true,
successRedirectUrl: true,
forwardParamsSuccessRedirect: true,
},
},
},
},
},
select: paymentDataSelect,
});
if (!rawPayment) return { notFound: true } as const;
@@ -0,0 +1,37 @@
import { useQuery } from "@tanstack/react-query";
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse } from "@calcom/platform-types";
import { useAtomsContext } from "../../hooks/useAtomsContext";
import http from "../../lib/http";
export const QUERY_KEY = "use-event-app-integration";
export const useAtomsEventTypePaymentInfo = ({
uid,
onEventTypePaymentInfoSuccess,
onEventTypePaymentInfoFailure,
}: {
uid: string;
onEventTypePaymentInfoSuccess?: () => void;
onEventTypePaymentInfoFailure?: () => void;
}) => {
const pathname = `/atoms/payment/${uid}`;
const { isInit, accessToken } = useAtomsContext();
return useQuery({
queryKey: [QUERY_KEY, uid],
queryFn: () => {
return http?.get<ApiResponse<Omit<PaymentPageProps, "trpcState">>>(pathname).then((res) => {
if (res.data.status === SUCCESS_STATUS) {
onEventTypePaymentInfoSuccess?.();
return res.data.data;
}
onEventTypePaymentInfoFailure?.();
throw new Error(res.data.error.message);
});
},
enabled: !!uid && isInit && !!accessToken,
});
};
@@ -175,6 +175,7 @@ function getNavigation({ length, multipleDuration, t, tabs, url, onClick, curren
tabsNavigation.push({
name: "event_payments_tab_title",
onClick: () => onClick("payments"),
isActive: currentTab === "payments",
href: `${url}?tabName=payments`,
icon: "credit-card",
info: `event_payments_tab_description`,
@@ -0,0 +1,151 @@
import React, { Suspense } from "react";
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import { PayIcon } from "@calcom/features/bookings/components/event-meta/PayIcon";
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
import { cn } from "../../src/lib/utils";
import { useAtomsEventTypePaymentInfo } from "../hooks/useAtomEventTypePaymentInfo";
const StripePaymentForm = React.lazy(() => import("./StripePaymentForm"));
export const PaymentForm = ({
paymentUid,
onPaymentSuccess,
onPaymentCancellation,
onEventTypePaymentInfoSuccess,
onEventTypePaymentInfoFailure,
}: {
paymentUid: string;
onPaymentSuccess?: (input: Omit<PaymentPageProps, "trpcState">) => void;
onPaymentCancellation?: (input: Omit<PaymentPageProps, "trpcState">) => void;
onEventTypePaymentInfoSuccess?: () => void;
onEventTypePaymentInfoFailure?: () => void;
}) => {
const { t } = useLocale();
const { data: paymentInfo, isLoading } = useAtomsEventTypePaymentInfo({
uid: paymentUid,
onEventTypePaymentInfoSuccess,
onEventTypePaymentInfoFailure,
});
const eventName = paymentInfo?.booking.title;
const is24h = isBrowserLocale24h();
const timezone =
localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess() || "Europe/London";
const date = dayjs.utc(paymentInfo?.booking.startTime).tz(timezone);
if (isLoading) return <h1 className="p-4 pt-4 text-xl">Loading...</h1>;
if (!paymentInfo) return <h1 className="p-4 text-xl">No payment found with UID - {paymentUid}</h1>;
const paymentAppData = getPaymentAppData(paymentInfo?.eventType);
return (
<AtomsWrapper>
<div>
<Suspense fallback={<></>}>
<main className="mx-auto">
<div>
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div className="inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div
className={cn(
"main bg-default border-subtle inline-block transform overflow-hidden rounded-lg border px-8 pb-4 pt-5 text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle"
)}
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div>
<div className="bg-success mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<PayIcon currency={paymentAppData.currency} className="h-8 w-8 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-2xl font-semibold leading-6" id="modal-headline">
{paymentAppData.paymentOption === "HOLD"
? t("complete_your_booking")
: t("payment")}
</h3>
<div className="text-default mt-4 grid grid-cols-3 border-b border-t py-4 text-left dark:border-gray-900 dark:text-gray-300">
<div className="font-medium">{t("what")}</div>
<div className="col-span-2 mb-6">{eventName}</div>
<div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6">
{date.format("dddd, DD MMMM YYYY")}
<br />
{date.format(is24h ? "H:mm" : "h:mma")} - {paymentInfo.eventType.length} mins{" "}
<span className="text-subtle">({timezone})</span>
</div>
{paymentInfo.booking.location && (
<>
<div className="font-medium">{t("where")}</div>
<div className="col-span-2 mb-6">
{getSuccessPageLocationMessage(paymentInfo.booking.location, t)}
</div>
</>
)}
<div className="font-medium">
{paymentInfo.payment.paymentOption === "HOLD" ? t("no_show_fee") : t("price")}
</div>
<div className="col-span-2 mb-6 font-semibold">
<Price
currency={paymentAppData.currency}
price={paymentAppData.price}
displayAlternateSymbol={false}
/>
</div>
</div>
</div>
</div>
<div>
{paymentInfo.payment.success && !paymentInfo.payment.refunded && (
<div className="text-default mt-4 text-center dark:text-gray-300">{t("paid")}</div>
)}
{paymentInfo.payment.appId === "stripe" && !paymentInfo.payment.success && (
<StripePaymentForm
clientSecret={paymentInfo.clientSecret}
payment={paymentInfo.payment}
eventType={paymentInfo.eventType}
user={paymentInfo.user}
location={paymentInfo.booking.location}
booking={paymentInfo.booking}
uid={paymentUid}
onPaymentSuccess={onPaymentSuccess}
onPaymentCancellation={onPaymentCancellation}
/>
)}
{paymentInfo.payment.refunded && (
<div className="text-default mt-4 text-center dark:text-gray-300">
{t("refunded")}
</div>
)}
</div>
{!paymentInfo.profile.hideBranding && (
<div className="text-muted dark:text-inverted mt-4 border-t pt-4 text-center text-xs dark:border-gray-900">
<a href={`${WEBSITE_URL}/signup`}>
{t("create_booking_link_with_calcom", { appName: APP_NAME })}
</a>
</div>
)}
</div>
</div>
</div>
</div>
</main>
</Suspense>
</div>
</AtomsWrapper>
);
};
@@ -0,0 +1,131 @@
import { Elements, useElements, useStripe } from "@stripe/react-stripe-js";
import { useEffect, useState } from "react";
import type { SyntheticEvent } from "react";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import type { Props, States } from "@calcom/features/ee/payments/components/Payment";
import { PaymentFormComponent } from "@calcom/features/ee/payments/components/Payment";
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
import { useLocale } from "@calcom/lib/hooks/useLocale";
const StripePaymentComponent = (
props: Props & {
onPaymentSuccess?: (input: Omit<PaymentPageProps, "trpcState">) => void;
onPaymentCancellation?: (input: Omit<PaymentPageProps, "trpcState">) => void;
}
) => {
const { t } = useLocale();
const elements = useElements();
const paymentOption = props.payment.paymentOption;
const attendeeEmail = props.booking.attendees[0].email;
const stripe = useStripe();
const [state, setState] = useState<States>({ status: "idle" });
return (
<PaymentFormComponent
{...props}
elements={elements}
paymentOption={paymentOption}
state={state}
onSubmit={async (ev: SyntheticEvent) => {
ev.preventDefault();
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: attendeeEmail,
};
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,
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 {
setState({ status: "idle" });
props.onPaymentSuccess?.(props as unknown as Omit<PaymentPageProps, "trpcState">);
if (props.location) {
if (props.location.includes("integration")) {
params.location = t("web_conferencing_details_to_follow");
} else {
params.location = props.location;
}
}
}
}}
onCancel={() => {
props.onPaymentCancellation?.(props as unknown as Omit<PaymentPageProps, "trpcState">);
}}
onPaymentElementChange={() => {
setState({ status: "idle" });
}}
/>
);
};
const StripePaymentForm = (
props: Props & {
uid: string;
onPaymentSuccess?: (input: Omit<PaymentPageProps, "trpcState">) => void;
onPaymentCancellation?: (input: Omit<PaymentPageProps, "trpcState">) => void;
}
) => {
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,
},
}}>
<StripePaymentComponent {...props} />
</Elements>
);
};
export default StripePaymentForm;
+1
View File
@@ -27,3 +27,4 @@ export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calc
export { EventTypePlatformWrapper as EventTypeSettings } from "./event-types/wrappers/EventTypePlatformWrapper";
export { StripeConnect } from "./connect/stripe/StripeConnect";
export { CreateEventTypePlatformWrapper as CreateEventType } from "./event-types/wrappers/CreateEventTypePlatformWrapper";
export { PaymentForm } from "./event-types/payments/PaymentForm";
@@ -93,7 +93,11 @@ export default function Bookings(props: { calUsername: string; calEmail: string
eventSlug={eventTypeSlug}
onCreateBookingSuccess={(data) => {
setBookingTitle(data.data.title ?? "");
router.push(`/${data.data.uid}`);
if (data.data.paymentRequired) {
router.push(`/payment/${data.data.paymentUid}`);
} else {
router.push(`/${data.data.uid}`);
}
}}
metadata={{ CustomKey: "CustomValue" }}
duration={eventTypeDuration}
@@ -0,0 +1,30 @@
import { Navbar } from "@/components/Navbar";
import { Inter } from "next/font/google";
import { usePathname } from "next/navigation";
import { useRouter } from "next/navigation";
import { PaymentForm } from "@calcom/atoms";
const inter = Inter({ subsets: ["latin"] });
export default function Payment(props: { calUsername: string; calEmail: string }) {
const pathname = usePathname();
const router = useRouter();
const uid = pathname.split("/").pop();
return (
<main className={`flex min-h-screen flex-col ${inter.className}`}>
<Navbar username={props.calUsername} />
<PaymentForm
paymentUid={uid ?? ""}
onPaymentSuccess={(data) => {
router.push("/bookings");
}}
onPaymentCancellation={(data) => {
router.back();
}}
/>
</main>
);
}
+5
View File
@@ -9,6 +9,7 @@ import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booki
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import * as newBookingMethods from "@calcom/features/bookings/lib/handleNewBooking";
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { handleCreatePhoneCall } from "@calcom/features/handleCreatePhoneCall";
import handleMarkNoShow from "@calcom/features/handleMarkNoShow";
@@ -19,6 +20,7 @@ import { symmetricEncrypt, symmetricDecrypt } from "@calcom/lib/crypto";
import { getTranslation } from "@calcom/lib/server/i18n";
import { MembershipRole } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { paymentDataSelect } from "@calcom/prisma/selects/payment";
import type { TeamQuery } from "@calcom/trpc/server/routers/loggedInViewer/integrations.handler";
import { updateHandler as updateScheduleHandler } from "@calcom/trpc/server/routers/viewer/availability/schedule/update.handler";
import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
@@ -175,3 +177,6 @@ export type { CredentialPayload };
export { getAppFromSlug };
export { credentialForCalendarServiceSelect };
export { MembershipRole };
export { paymentDataSelect };
export { getClientSecretFromPayment };
+64
View File
@@ -0,0 +1,64 @@
import { Prisma } from "@prisma/client";
export const paymentDataSelect = Prisma.validator<Prisma.PaymentSelect>()({
data: true,
success: true,
uid: true,
refunded: true,
bookingId: true,
appId: true,
amount: true,
currency: true,
paymentOption: true,
booking: {
select: {
id: true,
uid: true,
description: true,
title: true,
startTime: true,
endTime: true,
attendees: {
select: {
email: true,
name: true,
},
},
eventTypeId: true,
location: true,
status: true,
rejectionReason: true,
cancellationReason: true,
eventType: {
select: {
id: true,
title: true,
description: true,
length: true,
eventName: true,
requiresConfirmation: true,
userId: true,
metadata: true,
users: {
select: {
name: true,
username: true,
hideBranding: true,
theme: true,
},
},
team: {
select: {
name: true,
hideBranding: true,
},
},
price: true,
currency: true,
successRedirectUrl: true,
forwardParamsSuccessRedirect: true,
},
},
},
},
});