From a5191d1e5a61343c97d24ff7d47d5eb4633d2641 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Fri, 13 Sep 2024 12:13:33 -0400 Subject: [PATCH] chore: App-router-migration`(/auth/verify`) add missing metadata / page & extract components into `/module` (#16444) * create StripeRepository and simplify code * verify-email-change: extract getServerSideProps * add verify-email-change to app router * auth/verify: Add metadata in app router * auth/verify: extract client component into /modules * fix type checks for stripe * fix stripe session handler * improve getServerSideProps for verify-email-change * add title to verify-email-change page metadata * add title to verify page metadata * refactor * add safety * fix * remove references to pages for verify-email --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Keith Williams --- .../future/auth/verify-email-change/page.tsx | 22 ++ .../web/app/future/auth/verify-email/page.tsx | 7 +- apps/web/app/future/auth/verify/page.tsx | 42 ++- .../modules/auth/verify-email-change-view.tsx | 48 ++++ apps/web/modules/auth/verify-email-view.tsx | 59 ++++ apps/web/modules/auth/verify-view.tsx | 248 +++++++++++++++++ apps/web/pages/auth/verify-email-change.tsx | 99 +------ apps/web/pages/auth/verify-email.tsx | 64 +---- apps/web/pages/auth/verify.tsx | 253 +----------------- .../getServerSideProps.tsx | 52 ++++ packages/lib/server/repository/stripe.ts | 62 +++++ .../stripeCheckoutSession.handler.ts | 56 +--- 12 files changed, 553 insertions(+), 459 deletions(-) create mode 100644 apps/web/app/future/auth/verify-email-change/page.tsx create mode 100644 apps/web/modules/auth/verify-email-change-view.tsx create mode 100644 apps/web/modules/auth/verify-email-view.tsx create mode 100644 apps/web/modules/auth/verify-view.tsx create mode 100644 apps/web/server/lib/auth/verify-email-change/getServerSideProps.tsx create mode 100644 packages/lib/server/repository/stripe.ts diff --git a/apps/web/app/future/auth/verify-email-change/page.tsx b/apps/web/app/future/auth/verify-email-change/page.tsx new file mode 100644 index 0000000000..94628ea908 --- /dev/null +++ b/apps/web/app/future/auth/verify-email-change/page.tsx @@ -0,0 +1,22 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; +import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; + +import { getServerSideProps } from "@server/lib/auth/verify-email-change/getServerSideProps"; + +import type { PageProps } from "~/auth/verify-email-change-view"; +import VerifyEmailChange from "~/auth/verify-email-change-view"; + +export const generateMetadata = async () => { + return await _generateMetadata( + () => "Verify email change", + () => "" + ); +}; + +const getData = withAppDirSsr(getServerSideProps); +export default WithLayout({ + getLayout: null, + Page: VerifyEmailChange, + getData, +}); diff --git a/apps/web/app/future/auth/verify-email/page.tsx b/apps/web/app/future/auth/verify-email/page.tsx index a3f88601b0..a27759acbf 100644 --- a/apps/web/app/future/auth/verify-email/page.tsx +++ b/apps/web/app/future/auth/verify-email/page.tsx @@ -1,11 +1,12 @@ -import VerifyEmailPage from "@pages/auth/verify-email"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import VerifyEmailPage from "~/auth/verify-email-view"; + export const generateMetadata = async () => { return await _generateMetadata( - (t) => t("check_your_email"), - (t) => t("check_your_email") + () => "Verify email", + () => "" ); }; diff --git a/apps/web/app/future/auth/verify/page.tsx b/apps/web/app/future/auth/verify/page.tsx index 2fd6168250..1aceecd256 100644 --- a/apps/web/app/future/auth/verify/page.tsx +++ b/apps/web/app/future/auth/verify/page.tsx @@ -1,11 +1,49 @@ -import VerifyPage from "@pages/auth/verify"; import { withAppDirSsr } from "app/WithAppDirSsr"; +import type { PageProps as _PageProps } from "app/_types"; +import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { z } from "zod"; + +import { StripeRepository } from "@calcom/lib/server/repository/stripe"; import { getServerSideProps } from "@server/lib/auth/verify/getServerSideProps"; +import type { PageProps } from "~/auth/verify-view"; +import VerifyPage from "~/auth/verify-view"; + +const querySchema = z.object({ + stripeCustomerId: z.string().optional(), + sessionId: z.string().optional(), + t: z.string().optional(), +}); + +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { + const p = { ...params, ...searchParams }; + const { sessionId, stripeCustomerId } = querySchema.parse(p); + + if (!stripeCustomerId && !sessionId) { + return await _generateMetadata( + () => "Verify", + () => "" + ); + } + + const data = await StripeRepository.getCheckoutSession({ + stripeCustomerId, + checkoutSessionId: sessionId, + }); + + const { hasPaymentFailed } = data; + + return await _generateMetadata( + () => + hasPaymentFailed ? "Your payment failed" : sessionId ? "Payment successful!" : `Verify your email`, + () => "" + ); +}; + export default WithLayout({ getLayout: null, Page: VerifyPage, - getData: withAppDirSsr(getServerSideProps), + getData: withAppDirSsr(getServerSideProps), })<"P">; diff --git a/apps/web/modules/auth/verify-email-change-view.tsx b/apps/web/modules/auth/verify-email-change-view.tsx new file mode 100644 index 0000000000..ac98c66597 --- /dev/null +++ b/apps/web/modules/auth/verify-email-change-view.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import React from "react"; +import { Toaster } from "react-hot-toast"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { inferSSRProps } from "@calcom/types/inferSSRProps"; +import { showToast } from "@calcom/ui"; + +import type { getServerSideProps } from "@server/lib/auth/verify-email-change/getServerSideProps"; + +export type PageProps = inferSSRProps; + +function VerifyEmailChange(props: PageProps) { + const { update } = useSession(); + const { t, isLocaleReady } = useLocale(); + const router = useRouter(); + + useEffect(() => { + async function updateSessionAndDisplayToast() { + await update({ email: props.updatedEmail }); + if (isLocaleReady) { + showToast(t("verify_email_change_success_toast", { email: props.updatedEmail }), "success"); + } + router.push("/event-types"); + } + if (props.updateSession) { + updateSessionAndDisplayToast(); + } else { + if (isLocaleReady) { + showToast(t("verify_email_change_failure_toast"), "error"); + } + } + // We only need this to run on inital mount. These props can't and won't change due to it being fetched serveside. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ +
+ ); +} + +export default VerifyEmailChange; diff --git a/apps/web/modules/auth/verify-email-view.tsx b/apps/web/modules/auth/verify-email-view.tsx new file mode 100644 index 0000000000..69ffd27750 --- /dev/null +++ b/apps/web/modules/auth/verify-email-view.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +import { APP_NAME } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc"; +import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck"; +import { Button, EmptyScreen, showToast } from "@calcom/ui"; + +function VerifyEmailPage() { + const { data } = useEmailVerifyCheck(); + const { data: session } = useSession(); + const router = useRouter(); + const { t, isLocaleReady } = useLocale(); + const mutation = trpc.viewer.auth.resendVerifyEmail.useMutation(); + + useEffect(() => { + if (data?.isVerified) { + router.replace("/getting-started"); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data?.isVerified]); + if (!isLocaleReady) { + return null; + } + return ( +
+
+
+ { + showToast(t("send_email"), "success"); + mutation.mutate(); + }}> + {t("resend_email")} + + } + /> +
+
+
+ ); +} + +export default VerifyEmailPage; diff --git a/apps/web/modules/auth/verify-view.tsx b/apps/web/modules/auth/verify-view.tsx new file mode 100644 index 0000000000..327cefb2f4 --- /dev/null +++ b/apps/web/modules/auth/verify-view.tsx @@ -0,0 +1,248 @@ +"use client"; + +import { motion } from "framer-motion"; +import { signIn } from "next-auth/react"; +import Head from "next/head"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import z from "zod"; + +import { classNames } from "@calcom/lib"; +import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; +import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; +import { trpc } from "@calcom/trpc/react"; +import type { inferSSRProps } from "@calcom/types/inferSSRProps"; +import { Button, showToast } from "@calcom/ui"; +import { Icon } from "@calcom/ui"; + +import Loader from "@components/Loader"; + +import type { getServerSideProps } from "@server/lib/auth/verify/getServerSideProps"; + +export type PageProps = inferSSRProps; + +async function sendVerificationLogin(email: string, username: string) { + await signIn("email", { + email: email.toLowerCase(), + username: username.toLowerCase(), + redirect: false, + callbackUrl: WEBAPP_URL || "https://app.cal.com", + }) + .then(() => { + showToast("Verification email sent", "success"); + }) + .catch((err) => { + showToast(err, "error"); + }); +} + +function useSendFirstVerificationLogin({ + email, + username, +}: { + email: string | undefined; + username: string | undefined; +}) { + const sent = useRef(false); + useEffect(() => { + if (!email || !username || sent.current) { + return; + } + (async () => { + await sendVerificationLogin(email, username); + sent.current = true; + })(); + }, [email, username]); +} + +const querySchema = z.object({ + stripeCustomerId: z.string().optional(), + sessionId: z.string().optional(), + t: z.string().optional(), +}); + +const PaymentFailedIcon = () => ( +
+ +
+); + +const PaymentSuccess = () => ( +
+ + + + + + + +
+); + +const MailOpenIcon = () => ( +
+ +
+); + +export default function Verify(props: PageProps) { + const searchParams = useCompatSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + const routerQuery = useRouterQuery(); + const { t, sessionId, stripeCustomerId } = querySchema.parse(routerQuery); + const [secondsLeft, setSecondsLeft] = useState(30); + const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery( + { + stripeCustomerId, + checkoutSessionId: sessionId, + }, + { + enabled: !!stripeCustomerId || !!sessionId, + staleTime: Infinity, + } + ); + useSendFirstVerificationLogin({ email: data?.customer?.email, username: data?.customer?.username }); + // @note: check for t=timestamp and apply disabled state and secondsLeft accordingly + // to avoid refresh to skip waiting 30 seconds to re-send email + useEffect(() => { + const lastSent = new Date(parseInt(`${t}`)); + // @note: This double round() looks ugly but it's the only way I came up to get the time difference in seconds + const difference = Math.round(Math.round(new Date().getTime() - lastSent.getTime()) / 1000); + if (difference < 30) { + // If less than 30 seconds, set the seconds left to 30 - difference + setSecondsLeft(30 - difference); + } else { + // else set the seconds left to 0 and disabled false + setSecondsLeft(0); + } + }, [t]); + // @note: here we make sure each second is decremented if disabled up to 0. + useEffect(() => { + if (secondsLeft > 0) { + const interval = setInterval(() => { + if (secondsLeft > 0) { + setSecondsLeft(secondsLeft - 1); + } + }, 1000); + return () => clearInterval(interval); + } + }, [secondsLeft]); + + if (!data) { + // Loading state + return ; + } + const { valid, hasPaymentFailed, customer } = data; + if (!valid) { + throw new Error("Invalid session or customer id"); + } + + if (!stripeCustomerId && !sessionId) { + return
Invalid Link
; + } + + return ( +
+ + + {/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth + it or too hard to read. */} + {hasPaymentFailed + ? "Your payment failed" + : sessionId + ? "Payment successful!" + : `Verify your email | ${APP_NAME}`} + + +
+
+ {hasPaymentFailed ? : sessionId ? : } +

+ {hasPaymentFailed + ? "Your payment failed" + : sessionId + ? "Payment successful!" + : "Check your Inbox"} +

+ {hasPaymentFailed && ( +

Your account has been created, but your premium has not been reserved.

+ )} +

+ We have sent an email to {customer?.email} with a link to activate your account.{" "} + {hasPaymentFailed && + "Once you activate your account you will be able to try purchase your premium username again or select a different one."} +

+
+ +
+
+
+

Don’t seen an email?

+ +
+
+
+ ); +} diff --git a/apps/web/pages/auth/verify-email-change.tsx b/apps/web/pages/auth/verify-email-change.tsx index 378c9be03b..0013dc3593 100644 --- a/apps/web/pages/auth/verify-email-change.tsx +++ b/apps/web/pages/auth/verify-email-change.tsx @@ -1,96 +1,9 @@ -"use client"; - -import type { GetServerSidePropsContext } from "next"; -import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import { Toaster } from "react-hot-toast"; -import { z } from "zod"; - -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { showToast } from "@calcom/ui"; - import PageWrapper from "@components/PageWrapper"; -interface PageProps { - token: string; - updateSession: string; - updatedEmail: string; -} +import type { PageProps } from "~/auth/verify-email-change-view"; +import VerifyEmailChange from "~/auth/verify-email-change-view"; -function VerifyEmailChange(props: PageProps) { - const { update } = useSession(); - const { t, isLocaleReady } = useLocale(); - const router = useRouter(); - - useEffect(() => { - async function updateSessionAndDisplayToast() { - await update({ email: props.updatedEmail }); - if (isLocaleReady) { - showToast(t("verify_email_change_success_toast", { email: props.updatedEmail }), "success"); - } - router.push("/event-types"); - } - if (props.updateSession) { - updateSessionAndDisplayToast(); - } else { - if (isLocaleReady) { - showToast(t("verify_email_change_failure_toast"), "error"); - } - } - // We only need this to run on inital mount. These props can't and won't change due to it being fetched serveside. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
- -
- ); -} - -const tokenSchema = z.object({ - token: z.string(), -}); - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const { token } = tokenSchema.parse(context.query); - - if (!token) { - return { - notFound: true, - }; - } - - const params = new URLSearchParams({ - token, - }); - - const response = await fetch(`${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`, { - method: "POST", - }); - - if (!response.ok) { - return { - props: { - updateSession: false, - token, - updatedEmail: false, - }, - }; - } - - const data = await response.json(); - - return { - props: { - updateSession: true, - token, - updatedEmail: data.updatedEmail ?? null, - }, - }; -} - -export default VerifyEmailChange; -VerifyEmailChange.PageWrapper = PageWrapper; +const Page = (props: PageProps) => ; +Page.PageWrapper = PageWrapper; +export default Page; +export { getServerSideProps } from "@server/lib/auth/verify-email-change/getServerSideProps"; diff --git a/apps/web/pages/auth/verify-email.tsx b/apps/web/pages/auth/verify-email.tsx index 370dc41e3f..beaa5c47c4 100644 --- a/apps/web/pages/auth/verify-email.tsx +++ b/apps/web/pages/auth/verify-email.tsx @@ -1,63 +1,7 @@ -"use client"; - -import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - -import { APP_NAME } from "@calcom/lib/constants"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { trpc } from "@calcom/trpc"; -import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck"; -import { Button, EmptyScreen, showToast } from "@calcom/ui"; - import PageWrapper from "@components/PageWrapper"; -function VerifyEmailPage() { - const { data } = useEmailVerifyCheck(); - const { data: session } = useSession(); - const router = useRouter(); - const { t, isLocaleReady } = useLocale(); - const mutation = trpc.viewer.auth.resendVerifyEmail.useMutation(); +import VerifyEmail from "~/auth/verify-email-view"; - useEffect(() => { - if (data?.isVerified) { - router.replace("/getting-started"); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data?.isVerified]); - if (!isLocaleReady) { - return null; - } - return ( -
-
-
- { - showToast(t("send_email"), "success"); - mutation.mutate(); - }}> - {t("resend_email")} - - } - /> -
-
-
- ); -} - -export default VerifyEmailPage; - -VerifyEmailPage.PageWrapper = PageWrapper; +const Page = () => ; +Page.PageWrapper = PageWrapper; +export default Page; diff --git a/apps/web/pages/auth/verify.tsx b/apps/web/pages/auth/verify.tsx index 24c96f018a..9f25d00e2f 100644 --- a/apps/web/pages/auth/verify.tsx +++ b/apps/web/pages/auth/verify.tsx @@ -1,250 +1,9 @@ -"use client"; - -import { motion } from "framer-motion"; -import { signIn } from "next-auth/react"; -import Head from "next/head"; -import { usePathname, useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; -import z from "zod"; - -import { classNames } from "@calcom/lib"; -import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; -import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; -import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; -import { trpc } from "@calcom/trpc/react"; -import type { inferSSRProps } from "@calcom/types/inferSSRProps"; -import { Button, showToast } from "@calcom/ui"; -import { Icon } from "@calcom/ui"; - -import Loader from "@components/Loader"; import PageWrapper from "@components/PageWrapper"; -import { getServerSideProps } from "@server/lib/auth/verify/getServerSideProps"; +import type { PageProps } from "~/auth/verify-view"; +import Verify from "~/auth/verify-view"; -async function sendVerificationLogin(email: string, username: string) { - await signIn("email", { - email: email.toLowerCase(), - username: username.toLowerCase(), - redirect: false, - callbackUrl: WEBAPP_URL || "https://app.cal.com", - }) - .then(() => { - showToast("Verification email sent", "success"); - }) - .catch((err) => { - showToast(err, "error"); - }); -} - -function useSendFirstVerificationLogin({ - email, - username, -}: { - email: string | undefined; - username: string | undefined; -}) { - const sent = useRef(false); - useEffect(() => { - if (!email || !username || sent.current) { - return; - } - (async () => { - await sendVerificationLogin(email, username); - sent.current = true; - })(); - }, [email, username]); -} - -const querySchema = z.object({ - stripeCustomerId: z.string().optional(), - sessionId: z.string().optional(), - t: z.string().optional(), -}); - -const PaymentFailedIcon = () => ( -
- -
-); - -const PaymentSuccess = () => ( -
- - - - - - - -
-); - -const MailOpenIcon = () => ( -
- -
-); - -export default function Verify(props: inferSSRProps) { - const searchParams = useCompatSearchParams(); - const pathname = usePathname(); - const router = useRouter(); - const routerQuery = useRouterQuery(); - const { t, sessionId, stripeCustomerId } = querySchema.parse(routerQuery); - const [secondsLeft, setSecondsLeft] = useState(30); - const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery( - { - stripeCustomerId, - checkoutSessionId: sessionId, - }, - { - enabled: !!stripeCustomerId || !!sessionId, - staleTime: Infinity, - } - ); - useSendFirstVerificationLogin({ email: data?.customer?.email, username: data?.customer?.username }); - // @note: check for t=timestamp and apply disabled state and secondsLeft accordingly - // to avoid refresh to skip waiting 30 seconds to re-send email - useEffect(() => { - const lastSent = new Date(parseInt(`${t}`)); - // @note: This double round() looks ugly but it's the only way I came up to get the time difference in seconds - const difference = Math.round(Math.round(new Date().getTime() - lastSent.getTime()) / 1000); - if (difference < 30) { - // If less than 30 seconds, set the seconds left to 30 - difference - setSecondsLeft(30 - difference); - } else { - // else set the seconds left to 0 and disabled false - setSecondsLeft(0); - } - }, [t]); - // @note: here we make sure each second is decremented if disabled up to 0. - useEffect(() => { - if (secondsLeft > 0) { - const interval = setInterval(() => { - if (secondsLeft > 0) { - setSecondsLeft(secondsLeft - 1); - } - }, 1000); - return () => clearInterval(interval); - } - }, [secondsLeft]); - - if (!data) { - // Loading state - return ; - } - const { valid, hasPaymentFailed, customer } = data; - if (!valid) { - throw new Error("Invalid session or customer id"); - } - - if (!stripeCustomerId && !sessionId) { - return
Invalid Link
; - } - - return ( -
- - - {/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth - it or too hard to read. */} - {hasPaymentFailed - ? "Your payment failed" - : sessionId - ? "Payment successful!" - : `Verify your email | ${APP_NAME}`} - - -
-
- {hasPaymentFailed ? : sessionId ? : } -

- {hasPaymentFailed - ? "Your payment failed" - : sessionId - ? "Payment successful!" - : "Check your Inbox"} -

- {hasPaymentFailed && ( -

Your account has been created, but your premium has not been reserved.

- )} -

- We have sent an email to {customer?.email} with a link to activate your account.{" "} - {hasPaymentFailed && - "Once you activate your account you will be able to try purchase your premium username again or select a different one."} -

-
- -
-
-
-

Don’t seen an email?

- -
-
-
- ); -} - -export { getServerSideProps }; -Verify.PageWrapper = PageWrapper; +const Page = (props: PageProps) => ; +Page.PageWrapper = PageWrapper; +export default Page; +export { getServerSideProps } from "@server/lib/auth/verify/getServerSideProps"; diff --git a/apps/web/server/lib/auth/verify-email-change/getServerSideProps.tsx b/apps/web/server/lib/auth/verify-email-change/getServerSideProps.tsx new file mode 100644 index 0000000000..70183604a4 --- /dev/null +++ b/apps/web/server/lib/auth/verify-email-change/getServerSideProps.tsx @@ -0,0 +1,52 @@ +import type { GetServerSidePropsContext } from "next"; +import { z } from "zod"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; + +const tokenSchema = z.object({ + token: z.string(), +}); + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const parsed = tokenSchema.safeParse(context.query); + if (!parsed.success) { + return { + notFound: true, + } as const; + } + const { token } = parsed.data; + + if (!token) { + return { + notFound: true, + } as const; + } + + const params = new URLSearchParams({ + token, + }); + + const response = await fetch(`${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`, { + method: "POST", + }); + + if (!response.ok) { + return { + props: { + updateSession: false, + token, + updatedEmail: false, + }, + }; + } + + const data = await response.json(); + + return { + props: { + updateSession: true, + token, + updatedEmail: data.updatedEmail ?? null, + }, + }; +} diff --git a/packages/lib/server/repository/stripe.ts b/packages/lib/server/repository/stripe.ts new file mode 100644 index 0000000000..f8507ee13e --- /dev/null +++ b/packages/lib/server/repository/stripe.ts @@ -0,0 +1,62 @@ +import stripe from "@calcom/app-store/stripepayment/lib/server"; +import { + ZStripeCheckoutSessionInputSchema, + type TStripeCheckoutSessionInputSchema, +} from "@calcom/trpc/server/routers/publicViewer/stripeCheckoutSession.schema"; + +export class StripeRepository { + static async getCheckoutSession(input: TStripeCheckoutSessionInputSchema) { + const { checkoutSessionId, stripeCustomerId } = input; + + // Moved the following data checks to superRefine + ZStripeCheckoutSessionInputSchema.parse(input); + + let customerId: string; + let isPremiumUsername = false; + let hasPaymentFailed = false; + if (checkoutSessionId) { + try { + const session = await stripe.checkout.sessions.retrieve(checkoutSessionId); + if (typeof session.customer !== "string") { + return { + valid: false, + }; + } + customerId = session.customer; + isPremiumUsername = true; + hasPaymentFailed = session.payment_status !== "paid"; + } catch (e) { + return { + valid: false, + }; + } + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + customerId = stripeCustomerId!; + } + + try { + const customer = await stripe.customers.retrieve(customerId); + if (customer.deleted) { + return { + valid: false, + }; + } + + return { + valid: true, + hasPaymentFailed, + isPremiumUsername, + customer: { + username: customer.metadata.username, + email: customer.metadata.email, + stripeCustomerId: customerId, + }, + }; + } catch (e) { + return { + valid: false, + }; + } + } +} diff --git a/packages/trpc/server/routers/publicViewer/stripeCheckoutSession.handler.ts b/packages/trpc/server/routers/publicViewer/stripeCheckoutSession.handler.ts index 6ff5ac1aa6..6c6465ebb4 100644 --- a/packages/trpc/server/routers/publicViewer/stripeCheckoutSession.handler.ts +++ b/packages/trpc/server/routers/publicViewer/stripeCheckoutSession.handler.ts @@ -1,65 +1,13 @@ -import stripe from "@calcom/app-store/stripepayment/lib/server"; +import { StripeRepository } from "@calcom/lib/server/repository/stripe"; import type { TStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema"; -import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema"; type StripeCheckoutSessionOptions = { input: TStripeCheckoutSessionInputSchema; }; export const stripeCheckoutSessionHandler = async ({ input }: StripeCheckoutSessionOptions) => { - const { checkoutSessionId, stripeCustomerId } = input; - - // Moved the following data checks to superRefine - ZStripeCheckoutSessionInputSchema.parse(input); - - let customerId: string; - let isPremiumUsername = false; - let hasPaymentFailed = false; - if (checkoutSessionId) { - try { - const session = await stripe.checkout.sessions.retrieve(checkoutSessionId); - if (typeof session.customer !== "string") { - return { - valid: false, - }; - } - customerId = session.customer; - isPremiumUsername = true; - hasPaymentFailed = session.payment_status !== "paid"; - } catch (e) { - return { - valid: false, - }; - } - } else { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - customerId = stripeCustomerId!; - } - - try { - const customer = await stripe.customers.retrieve(customerId); - if (customer.deleted) { - return { - valid: false, - }; - } - - return { - valid: true, - hasPaymentFailed, - isPremiumUsername, - customer: { - username: customer.metadata.username, - email: customer.metadata.email, - stripeCustomerId: customerId, - }, - }; - } catch (e) { - return { - valid: false, - }; - } + return await StripeRepository.getCheckoutSession(input); }; export default stripeCheckoutSessionHandler;