fix: signup (#25334)

* Handle Stripe logic in `paymentCallback`

* Remove endpoint

* Do not send email verification email if premium username

* Remove logic from verify-view

* Callback send verification email

* Add `create` method to `VerificationToken` repository

* Create `VerificationTokenService`

* Early return if payment failed

* Refactor token generation

* Add tests

* Type fixes

* Type fixes
This commit is contained in:
Joe Au-Yeung authored and GitHub committed 2025-11-21 19:51:19 +00:00
1 parent 06348982a3
commit e5ebf7feb4
12 files changed
+714 -104

No files matched your search

+27 -32
View File
@@ -10,14 +10,11 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import Loader from "@components/Loader";
async function sendVerificationLogin(email: string, username: string, t: (key: string) => string) {
await signIn("email", {
email: email.toLowerCase(),
@@ -54,8 +51,9 @@ function useSendFirstVerificationLogin({
}
const querySchema = z.object({
stripeCustomerId: z.string().optional(),
sessionId: z.string().optional(),
email: z.string().optional(),
username: z.string().optional(),
paymentStatus: z.string().optional(),
t: z.string().optional(),
});
@@ -119,20 +117,20 @@ export default function Verify({ EMAIL_FROM }: { EMAIL_FROM?: string }) {
const pathname = usePathname();
const router = useRouter();
const routerQuery = useRouterQuery();
const { t: tParam, sessionId, stripeCustomerId } = querySchema.parse(routerQuery);
const { email, username, paymentStatus } = querySchema.parse(routerQuery);
const { t } = useLocale();
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 });
// Derive payment failed status from payment status
const hasPaymentFailed = paymentStatus !== undefined && paymentStatus !== "paid";
const isPremiumUsername = !!paymentStatus; // If paymentStatus exists, it's from premium username flow
// Only send verification login if we DON'T have the email yet (for resend button)
// The email is already sent server-side in paymentCallback
useSendFirstVerificationLogin({
email: !isPremiumUsername ? email : undefined,
username: !isPremiumUsername ? username : undefined,
});
// @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(() => {
@@ -159,16 +157,7 @@ export default function Verify({ EMAIL_FROM }: { EMAIL_FROM?: string }) {
}
}, [secondsLeft]);
if (!data) {
// Loading state
return <Loader />;
}
const { valid, hasPaymentFailed, customer } = data;
if (!valid) {
throw new Error("Invalid session or customer id");
}
if (!stripeCustomerId && !sessionId) {
if (!email) {
return <div>{t("invalid_link")}</div>;
}
@@ -176,17 +165,23 @@ export default function Verify({ EMAIL_FROM }: { EMAIL_FROM?: string }) {
<div className="text-default bg-muted bg-opacity-90 backdrop-blur-md backdrop-grayscale backdrop-filter">
<div className="flex min-h-screen flex-col items-center justify-center px-6">
<div className="border-subtle bg-default m-10 flex max-w-2xl flex-col items-center rounded-xl border px-8 py-14 text-left">
{hasPaymentFailed ? <PaymentFailedIcon /> : sessionId ? <PaymentSuccess /> : <MailOpenIcon />}
{hasPaymentFailed ? (
<PaymentFailedIcon />
) : isPremiumUsername ? (
<PaymentSuccess />
) : (
<MailOpenIcon />
)}
<h3 className="font-cal text-emphasis my-6 text-2xl font-normal leading-none">
{hasPaymentFailed
? t("your_payment_failed")
: sessionId
: isPremiumUsername
? t("payment_successful")
: t("check_your_inbox")}
</h3>
{hasPaymentFailed && <p className="my-6">{t("account_created_premium_not_reserved")}</p>}
<p className="text-muted dark:text-subtle text-base font-normal">
{t("email_sent_with_activation_link", { email: customer?.email })}{" "}
{t("email_sent_with_activation_link", { email })}{" "}
{hasPaymentFailed && t("activate_account_to_purchase_username")}
</p>
<div className="mt-7">
@@ -212,7 +207,7 @@ export default function Verify({ EMAIL_FROM }: { EMAIL_FROM?: string }) {
)}
disabled={secondsLeft > 0}
onClick={async (e) => {
if (!customer) {
if (!email || !username) {
return;
}
e.preventDefault();
@@ -221,7 +216,7 @@ export default function Verify({ EMAIL_FROM }: { EMAIL_FROM?: string }) {
const _searchParams = new URLSearchParams(searchParams?.toString());
_searchParams.set("t", `${Date.now()}`);
router.replace(`${pathname}?${_searchParams.toString()}`);
return await sendVerificationLogin(customer.email, customer.username, t);
return await sendVerificationLogin(email, username, t);
}}>
{secondsLeft > 0 ? t("resend_in_seconds", { seconds: secondsLeft }) : t("resend")}
</button>