diff --git a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts index 47797ddfa4..825766b47c 100644 --- a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts +++ b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts @@ -1,17 +1,15 @@ -import type { Mock } from "vitest"; -import { vi } from "vitest"; - import type { MockResponse } from "@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"; - import { prismaMock, resetPrismaMock, } from "@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"; -import { - createMockTeam, - createMockFoundToken, -} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories"; import type { SignupBody } from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories"; +import { + createMockFoundToken, + createMockTeam, +} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories"; +import type { Mock } from "vitest"; +import { vi } from "vitest"; const mockFindTokenByToken: Mock = vi.fn(); const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn(); @@ -77,6 +75,22 @@ vi.mock("@calcom/features/watchlist/lib/telemetry", () => ({ sentrySpan: {} })); vi.mock("@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller", () => ({ checkIfEmailIsBlockedInWatchlistController: vi.fn().mockResolvedValue(false), })); +vi.mock("@calcom/features/di/containers/FeatureRepository", () => ({ + getFeatureRepository: vi.fn().mockReturnValue({ + checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false), + }), +})); +vi.mock("@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository", () => { + return { + GlobalWatchlistRepository: class { + findBlockedEmail = vi.fn().mockResolvedValue(null); + createEntry = vi.fn().mockResolvedValue({}); + }, + }; +}); +vi.mock("@calcom/features/watchlist/lib/utils/normalization", () => ({ + normalizeEmail: vi.fn((e: string) => e.toLowerCase()), +})); vi.mock("@calcom/web/lib/buildLegacyCtx", () => ({ buildLegacyRequest: vi.fn() })); vi.mock("@calcom/features/auth/signup/utils/organization", () => ({ joinAnyChildTeamOnOrgInvite: vi.fn() })); vi.mock("@calcom/features/auth/signup/utils/token", () => ({ diff --git a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts index 3be4611997..2c0b63f0e1 100644 --- a/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts +++ b/apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts @@ -1,14 +1,23 @@ -import { cookies, headers } from "next/headers"; -import { NextResponse } from "next/server"; - +import process from "node:process"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromRequest"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; +import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants"; import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships"; +import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization"; import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar"; +import { + findTokenByToken, + throwIfTokenExpired, + validateAndGetCorrectedUsernameForTeam, +} from "@calcom/features/auth/signup/utils/token"; import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername"; import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing"; +import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository"; +import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; +import { GlobalWatchlistRepository } from "@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository"; import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry"; +import { normalizeEmail } from "@calcom/features/watchlist/lib/utils/normalization"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -19,19 +28,17 @@ import type { CustomNextApiHandler } from "@calcom/lib/server/username"; import { usernameHandler } from "@calcom/lib/server/username"; import { getTrackingFromCookies } from "@calcom/lib/tracking"; import prisma from "@calcom/prisma"; -import { CreationSource } from "@calcom/prisma/enums"; -import { IdentityProvider } from "@calcom/prisma/enums"; +import { + CreationSource, + IdentityProvider, + WatchlistAction, + WatchlistSource, + WatchlistType, +} from "@calcom/prisma/enums"; import { signupSchema } from "@calcom/prisma/zod-utils"; import { buildLegacyRequest } from "@calcom/web/lib/buildLegacyCtx"; - -import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization"; -import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants"; - -import { - findTokenByToken, - throwIfTokenExpired, - validateAndGetCorrectedUsernameForTeam, -} from "@calcom/features/auth/signup/utils/token"; +import { cookies, headers } from "next/headers"; +import { NextResponse } from "next/server"; const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] }); @@ -291,6 +298,34 @@ const handler: CustomNextApiHandler = async (body, usernameStatus, query) => { } } + const featureRepository = getFeatureRepository(); + const signupWatchlistReviewEnabled = + await featureRepository.checkIfFeatureIsEnabledGlobally("signup-watchlist-review"); + + if (signupWatchlistReviewEnabled && !token) { + const globalWatchlistRepo = new GlobalWatchlistRepository(prisma); + const normalizedEmail = normalizeEmail(email); + const existing = await globalWatchlistRepo.findBlockedEmail(normalizedEmail); + + if (!existing) { + await globalWatchlistRepo.createEntry({ + type: WatchlistType.EMAIL, + value: normalizedEmail, + action: WatchlistAction.BLOCK, + source: WatchlistSource.SIGNUP, + description: "Auto-added during signup review", + }); + } + + const userRepository = new UserRepository(prisma); + await userRepository.lockByEmail({ email }); + + return NextResponse.json( + { message: "Created user", stripeCustomerId: customer.stripeCustomerId, accountUnderReview: true }, + { status: 201 } + ); + } + if (checkoutSessionId) { console.log("Created user but missing payment", checkoutSessionId); return NextResponse.json( diff --git a/apps/web/modules/feature-flags/hooks/useFlags.ts b/apps/web/modules/feature-flags/hooks/useFlags.ts index 59e6438255..9038e72b23 100644 --- a/apps/web/modules/feature-flags/hooks/useFlags.ts +++ b/apps/web/modules/feature-flags/hooks/useFlags.ts @@ -37,6 +37,7 @@ const initialData: AppFlags = { "hwm-seating": false, "active-user-billing": false, "sidebar-tips": false, + "signup-watchlist-review": false, }; if (process.env.NEXT_PUBLIC_IS_E2E) { diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index 26f6313011..e72fbde1b3 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -1,35 +1,22 @@ "use client"; -import { Analytics as DubAnalytics } from "@dub/analytics/react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { signIn } from "next-auth/react"; -import dynamic from "next/dynamic"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import Script from "next/script"; -import posthog from "posthog-js"; -import { useState, useEffect } from "react"; -import type { SubmitHandler } from "react-hook-form"; -import { useForm, useFormContext } from "react-hook-form"; -import { Toaster } from "sonner"; -import { z } from "zod"; - import getStripe from "@calcom/app-store/stripepayment/lib/client"; import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils"; import { fetchSignup, - isUserAlreadyExistsError, hasCheckoutSession, + isAccountUnderReview, + isUserAlreadyExistsError, } from "@calcom/features/auth/signup/lib/fetchSignup"; import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import ServerTrans from "@calcom/lib/components/ServerTrans"; import { APP_NAME, - URL_PROTOCOL_REGEX, - IS_CALCOM, - WEBAPP_URL, CLOUDFLARE_SITE_ID, + IS_CALCOM, + URL_PROTOCOL_REGEX, + WEBAPP_URL, WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL, WEBSITE_URL, @@ -47,12 +34,24 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import classNames from "@calcom/ui/classNames"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; -import { PasswordField, CheckboxField, TextField, Form, SelectField } from "@calcom/ui/components/form"; +import { CheckboxField, Form, PasswordField, SelectField, TextField } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; -import { InfoIcon, ShieldCheckIcon, StarIcon } from "@coss/ui/icons"; import { showToast } from "@calcom/ui/components/toast"; - +import { InfoIcon, ShieldCheckIcon, StarIcon } from "@coss/ui/icons"; +import { Analytics as DubAnalytics } from "@dub/analytics/react"; +import { zodResolver } from "@hookform/resolvers/zod"; import type { getServerSideProps } from "@lib/signup/getServerSideProps"; +import dynamic from "next/dynamic"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import Script from "next/script"; +import { signIn } from "next-auth/react"; +import posthog from "posthog-js"; +import { useEffect, useState } from "react"; +import type { SubmitHandler } from "react-hook-form"; +import { useForm, useFormContext } from "react-hook-form"; +import { Toaster } from "sonner"; +import { z } from "zod"; const signupSchema = apiSignupSchema.extend({ apiError: z.string().optional(), // Needed to display API errors doesn't get passed to the API @@ -199,6 +198,7 @@ export default function Signup({ const [premiumUsername, setPremiumUsername] = useState(false); const [usernameTaken, setUsernameTaken] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const [accountUnderReview, setAccountUnderReview] = useState(false); const [displayEmailForm, setDisplayEmailForm] = useState(token); const [turnstileKey, setTurnstileKey] = useState(0); const searchParams = useCompatSearchParams(); @@ -277,6 +277,11 @@ export default function Signup({ throw new Error(result.error.message); } + if (isAccountUnderReview(result)) { + setAccountUnderReview(true); + return; + } + if (process.env.NEXT_PUBLIC_GTM_ID) { pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language }); } @@ -378,388 +383,409 @@ export default function Signup({