From fee3b73bda8500cb00b880d69d62d80ce8605b14 Mon Sep 17 00:00:00 2001 From: Rajiv Sahal Date: Thu, 31 Oct 2024 14:51:13 +0530 Subject: [PATCH] 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 --- apps/api/v2/src/modules/atoms/atoms.module.ts | 2 + .../v2/src/modules/atoms/atoms.repository.ts | 17 ++ .../atoms/controllers/atoms.controller.ts | 12 ++ .../services/event-types-atom.service.ts | 43 +++++ .../ee/payments/components/Payment.tsx | 167 +++++++++++------- .../features/ee/payments/pages/payment.tsx | 64 +------ .../hooks/useAtomEventTypePaymentInfo.ts | 37 ++++ .../hooks/usePlatformTabsNavigations.tsx | 1 + .../event-types/payments/PaymentForm.tsx | 151 ++++++++++++++++ .../payments/StripePaymentForm.tsx | 131 ++++++++++++++ packages/platform/atoms/index.ts | 1 + .../examples/base/src/pages/booking.tsx | 6 +- .../base/src/pages/payment/[paymentUid].tsx | 30 ++++ packages/platform/libraries/index.ts | 5 + packages/prisma/selects/payment.ts | 64 +++++++ 15 files changed, 602 insertions(+), 129 deletions(-) create mode 100644 apps/api/v2/src/modules/atoms/atoms.repository.ts create mode 100644 packages/platform/atoms/event-types/hooks/useAtomEventTypePaymentInfo.ts create mode 100644 packages/platform/atoms/event-types/payments/PaymentForm.tsx create mode 100644 packages/platform/atoms/event-types/payments/StripePaymentForm.tsx create mode 100644 packages/platform/examples/base/src/pages/payment/[paymentUid].tsx create mode 100644 packages/prisma/selects/payment.ts diff --git a/apps/api/v2/src/modules/atoms/atoms.module.ts b/apps/api/v2/src/modules/atoms/atoms.module.ts index ab7f33f7ed..157913db3a 100644 --- a/apps/api/v2/src/modules/atoms/atoms.module.ts +++ b/apps/api/v2/src/modules/atoms/atoms.module.ts @@ -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, ], diff --git a/apps/api/v2/src/modules/atoms/atoms.repository.ts b/apps/api/v2/src/modules/atoms/atoms.repository.ts new file mode 100644 index 0000000000..c0d1ea534e --- /dev/null +++ b/apps/api/v2/src/modules/atoms/atoms.repository.ts @@ -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, + }); + } +} diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts index 72f7ef43ff..dcfb6253ac 100644 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts +++ b/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts @@ -72,6 +72,18 @@ export class AtomsController { }; } + @Get("payment/:uid") + @Version(VERSION_NEUTRAL) + @UseGuards(ApiAuthGuard) + async getUserPaymentInfoById(@Param("uid") uid: string): Promise> { + const data = await this.eventTypesService.getUserPaymentInfo(uid); + + return { + status: SUCCESS_STATUS, + data, + }; + } + @Patch("event-types/:eventTypeId") @Version(VERSION_NEUTRAL) @UseGuards(ApiAuthGuard) diff --git a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts index f8fceb2401..0d86ca8671 100644 --- a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts +++ b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts @@ -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, + }; + 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, + }; + } } diff --git a/packages/features/ee/payments/components/Payment.tsx b/packages/features/ee/payments/components/Payment.tsx index f91440f96b..65d8b63fe2 100644 --- a/packages/features/ee/payments/components/Payment.tsx +++ b/packages/features/ee/payments/components/Payment.tsx @@ -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 & { data: Record; }; @@ -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(false); + const [holdAcknowledged, setHoldAcknowledged] = useState(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 ( +
+
+ onPaymentElementChange()} /> +
+ {paymentOption === "HOLD" && ( +
+ setHoldAcknowledged(e.target.checked)} + descriptionClassName="text-info font-semibold" + /> +
+ )} +
+ + +
+ {state.status === "error" && ( +
+ {state.error.message} +
+ )} +
+ ); +}; + 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({ status: "idle" }); - const [isCanceling, setIsCanceling] = useState(false); const stripe = useStripe(); const elements = useElements(); const paymentOption = props.payment.paymentOption; - const [holdAcknowledged, setHoldAcknowledged] = useState(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 ( -
-
- setState({ status: "idle" })} /> -
- {paymentOption === "HOLD" && ( -
- setHoldAcknowledged(e.target.checked)} - descriptionClassName="text-info font-semibold" - /> -
- )} -
- - -
- {state.status === "error" && ( -
- {state.error.message} -
- )} -
+ { + if (username) { + return router.push(`/${username}`); + } + return router.back(); + }} + onPaymentElementChange={() => { + setState({ status: "idle" }); + }} + /> ); }; diff --git a/packages/features/ee/payments/pages/payment.tsx b/packages/features/ee/payments/pages/payment.tsx index f0fc011bfc..7ab1f8ce71 100644 --- a/packages/features/ee/payments/pages/payment.tsx +++ b/packages/features/ee/payments/pages/payment.tsx @@ -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; diff --git a/packages/platform/atoms/event-types/hooks/useAtomEventTypePaymentInfo.ts b/packages/platform/atoms/event-types/hooks/useAtomEventTypePaymentInfo.ts new file mode 100644 index 0000000000..4b7de5b97e --- /dev/null +++ b/packages/platform/atoms/event-types/hooks/useAtomEventTypePaymentInfo.ts @@ -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>>(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, + }); +}; diff --git a/packages/platform/atoms/event-types/hooks/usePlatformTabsNavigations.tsx b/packages/platform/atoms/event-types/hooks/usePlatformTabsNavigations.tsx index d9be3fccc8..4265dbda0d 100644 --- a/packages/platform/atoms/event-types/hooks/usePlatformTabsNavigations.tsx +++ b/packages/platform/atoms/event-types/hooks/usePlatformTabsNavigations.tsx @@ -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`, diff --git a/packages/platform/atoms/event-types/payments/PaymentForm.tsx b/packages/platform/atoms/event-types/payments/PaymentForm.tsx new file mode 100644 index 0000000000..73d9d4befa --- /dev/null +++ b/packages/platform/atoms/event-types/payments/PaymentForm.tsx @@ -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) => void; + onPaymentCancellation?: (input: Omit) => 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

Loading...

; + + if (!paymentInfo) return

No payment found with UID - {paymentUid}

; + + const paymentAppData = getPaymentAppData(paymentInfo?.eventType); + + return ( + +
+ }> +
+
+
+ +
+
+
+
+
+
+ ); +}; diff --git a/packages/platform/atoms/event-types/payments/StripePaymentForm.tsx b/packages/platform/atoms/event-types/payments/StripePaymentForm.tsx new file mode 100644 index 0000000000..1f589fb32e --- /dev/null +++ b/packages/platform/atoms/event-types/payments/StripePaymentForm.tsx @@ -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) => void; + onPaymentCancellation?: (input: Omit) => 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({ status: "idle" }); + + return ( + { + 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); + 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); + }} + onPaymentElementChange={() => { + setState({ status: "idle" }); + }} + /> + ); +}; + +const StripePaymentForm = ( + props: Props & { + uid: string; + onPaymentSuccess?: (input: Omit) => void; + onPaymentCancellation?: (input: Omit) => 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 ( + + + + ); +}; + +export default StripePaymentForm; diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index 6d8184e285..d7b4bc17c6 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -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"; diff --git a/packages/platform/examples/base/src/pages/booking.tsx b/packages/platform/examples/base/src/pages/booking.tsx index 82a7f95d0b..e0842cd65e 100644 --- a/packages/platform/examples/base/src/pages/booking.tsx +++ b/packages/platform/examples/base/src/pages/booking.tsx @@ -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} diff --git a/packages/platform/examples/base/src/pages/payment/[paymentUid].tsx b/packages/platform/examples/base/src/pages/payment/[paymentUid].tsx new file mode 100644 index 0000000000..3a3fb22d19 --- /dev/null +++ b/packages/platform/examples/base/src/pages/payment/[paymentUid].tsx @@ -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 ( +
+ + { + router.push("/bookings"); + }} + onPaymentCancellation={(data) => { + router.back(); + }} + /> +
+ ); +} diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 83d97aa8e8..1b6ccfc3d7 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -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 }; diff --git a/packages/prisma/selects/payment.ts b/packages/prisma/selects/payment.ts new file mode 100644 index 0000000000..900209fe93 --- /dev/null +++ b/packages/prisma/selects/payment.ts @@ -0,0 +1,64 @@ +import { Prisma } from "@prisma/client"; + +export const paymentDataSelect = Prisma.validator()({ + 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, + }, + }, + }, + }, +});