Skip to content

Commit d3bbed0

Browse files
emrysaldevin-ai-integration[bot]cubic-dev-ai[bot]
authored
feat: add signup watchlist review mode (calcom#27912)
* feat: add signup watchlist review feature flag and handler logic - Add 'signup-watchlist-review' global feature flag - Add SIGNUP to WatchlistSource enum in Prisma schema - When flag enabled, lock new signups and add email to watchlist - Show 'account under review' message on signup page - Add i18n strings for review UI - Create seed migration for the feature flag Co-Authored-By: alex@cal.com <me@alexvanandel.com> * test: add isAccountUnderReview tests to fetchSignup test suite Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: address Cubic AI review feedback (confidence >= 9/10) - Remove 'import process from node:process' in signup-view.tsx (P0 bug in 'use client' component) - Move watchlist review check before checkoutSessionId early return in calcomSignupHandler (P1 premium bypass) - Revert selfHostedHandler to original state (out of scope per user request) - Add test mocks for FeaturesRepository and GlobalWatchlistRepository Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: remove node:process import from useFlags.ts (client-side file) Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: remove !token condition from watchlist review check Token is present in normal email-verified signups, so the !token condition was incorrectly skipping watchlist review for verified users. Co-Authored-By: alex@cal.com <me@alexvanandel.com> * Apply suggestion from @cubic-dev-ai[bot] Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * refactor: move user lock to UserRepository.lockByEmail Co-Authored-By: alex@cal.com <me@alexvanandel.com> * refactor: use cached getFeatureRepository() instead of deprecated FeaturesRepository Co-Authored-By: alex@cal.com <me@alexvanandel.com> * refactor: remove user locking, keep only watchlist addition on signup review Co-Authored-By: alex@cal.com <me@alexvanandel.com> * feat: lock user on signup review, remove watchlist entry on unlock Co-Authored-By: alex@cal.com <me@alexvanandel.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent 9dfcb0d commit d3bbed0

12 files changed

Lines changed: 565 additions & 411 deletions

File tree

apps/web/app/api/auth/signup/handlers/calcomSignupHandler.test.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import type { Mock } from "vitest";
2-
import { vi } from "vitest";
3-
41
import type { MockResponse } from "@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks";
5-
62
import {
73
prismaMock,
84
resetPrismaMock,
95
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks";
6+
import type { SignupBody } from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
107
import {
11-
createMockTeam,
128
createMockFoundToken,
9+
createMockTeam,
1310
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
14-
import type { SignupBody } from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
11+
import type { Mock } from "vitest";
12+
import { vi } from "vitest";
1513

1614
const mockFindTokenByToken: Mock = vi.fn();
1715
const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn();
@@ -77,6 +75,22 @@ vi.mock("@calcom/features/watchlist/lib/telemetry", () => ({ sentrySpan: {} }));
7775
vi.mock("@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller", () => ({
7876
checkIfEmailIsBlockedInWatchlistController: vi.fn().mockResolvedValue(false),
7977
}));
78+
vi.mock("@calcom/features/di/containers/FeatureRepository", () => ({
79+
getFeatureRepository: vi.fn().mockReturnValue({
80+
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false),
81+
}),
82+
}));
83+
vi.mock("@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository", () => {
84+
return {
85+
GlobalWatchlistRepository: class {
86+
findBlockedEmail = vi.fn().mockResolvedValue(null);
87+
createEntry = vi.fn().mockResolvedValue({});
88+
},
89+
};
90+
});
91+
vi.mock("@calcom/features/watchlist/lib/utils/normalization", () => ({
92+
normalizeEmail: vi.fn((e: string) => e.toLowerCase()),
93+
}));
8094
vi.mock("@calcom/web/lib/buildLegacyCtx", () => ({ buildLegacyRequest: vi.fn() }));
8195
vi.mock("@calcom/features/auth/signup/utils/organization", () => ({ joinAnyChildTeamOnOrgInvite: vi.fn() }));
8296
vi.mock("@calcom/features/auth/signup/utils/token", () => ({

apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
import { cookies, headers } from "next/headers";
2-
import { NextResponse } from "next/server";
3-
1+
import process from "node:process";
42
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
53
import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromRequest";
64
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
5+
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
76
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
7+
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
88
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
9+
import {
10+
findTokenByToken,
11+
throwIfTokenExpired,
12+
validateAndGetCorrectedUsernameForTeam,
13+
} from "@calcom/features/auth/signup/utils/token";
914
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername";
1015
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
16+
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
17+
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
18+
import { GlobalWatchlistRepository } from "@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository";
1119
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
20+
import { normalizeEmail } from "@calcom/features/watchlist/lib/utils/normalization";
1221
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
1322
import { hashPassword } from "@calcom/lib/auth/hashPassword";
1423
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -19,19 +28,17 @@ import type { CustomNextApiHandler } from "@calcom/lib/server/username";
1928
import { usernameHandler } from "@calcom/lib/server/username";
2029
import { getTrackingFromCookies } from "@calcom/lib/tracking";
2130
import prisma from "@calcom/prisma";
22-
import { CreationSource } from "@calcom/prisma/enums";
23-
import { IdentityProvider } from "@calcom/prisma/enums";
31+
import {
32+
CreationSource,
33+
IdentityProvider,
34+
WatchlistAction,
35+
WatchlistSource,
36+
WatchlistType,
37+
} from "@calcom/prisma/enums";
2438
import { signupSchema } from "@calcom/prisma/zod-utils";
2539
import { buildLegacyRequest } from "@calcom/web/lib/buildLegacyCtx";
26-
27-
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
28-
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
29-
30-
import {
31-
findTokenByToken,
32-
throwIfTokenExpired,
33-
validateAndGetCorrectedUsernameForTeam,
34-
} from "@calcom/features/auth/signup/utils/token";
40+
import { cookies, headers } from "next/headers";
41+
import { NextResponse } from "next/server";
3542

3643
const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] });
3744

@@ -291,6 +298,34 @@ const handler: CustomNextApiHandler = async (body, usernameStatus, query) => {
291298
}
292299
}
293300

301+
const featureRepository = getFeatureRepository();
302+
const signupWatchlistReviewEnabled =
303+
await featureRepository.checkIfFeatureIsEnabledGlobally("signup-watchlist-review");
304+
305+
if (signupWatchlistReviewEnabled && !token) {
306+
const globalWatchlistRepo = new GlobalWatchlistRepository(prisma);
307+
const normalizedEmail = normalizeEmail(email);
308+
const existing = await globalWatchlistRepo.findBlockedEmail(normalizedEmail);
309+
310+
if (!existing) {
311+
await globalWatchlistRepo.createEntry({
312+
type: WatchlistType.EMAIL,
313+
value: normalizedEmail,
314+
action: WatchlistAction.BLOCK,
315+
source: WatchlistSource.SIGNUP,
316+
description: "Auto-added during signup review",
317+
});
318+
}
319+
320+
const userRepository = new UserRepository(prisma);
321+
await userRepository.lockByEmail({ email });
322+
323+
return NextResponse.json(
324+
{ message: "Created user", stripeCustomerId: customer.stripeCustomerId, accountUnderReview: true },
325+
{ status: 201 }
326+
);
327+
}
328+
294329
if (checkoutSessionId) {
295330
console.log("Created user but missing payment", checkoutSessionId);
296331
return NextResponse.json(

apps/web/modules/feature-flags/hooks/useFlags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const initialData: AppFlags = {
3737
"hwm-seating": false,
3838
"active-user-billing": false,
3939
"sidebar-tips": false,
40+
"signup-watchlist-review": false,
4041
};
4142

4243
if (process.env.NEXT_PUBLIC_IS_E2E) {

0 commit comments

Comments
 (0)