Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
add waitlist submission form by KMKoushik · Pull Request #238 · usesend/useSend · GitHub
Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' add waitlist submission form by KMKoushik · Pull Request #238 · usesend/useSend · GitHub
Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' add waitlist submission form by KMKoushik · Pull Request #238 · usesend/useSend · GitHub
Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' add waitlist submission form by KMKoushik · Pull Request #238 · usesend/useSend · GitHub
Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' add waitlist submission form by KMKoushik · Pull Request #238 · usesend/useSend · GitHub
Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' add waitlist submission form by KMKoushik · Pull Request #238 · usesend/useSend · GitHub
Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); add waitlist submission form by KMKoushik · Pull Request #238 · usesend/useSend · GitHub
Skip to content

add waitlist submission form - #238

Merged
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form
Sep 18, 2025
Merged

add waitlist submission form#238
KMKoushik merged 2 commits into
mainfrom
km/2025-09-18-waitlist-form

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Sep 18, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Admin dashboard layout with navigation; added Teams and Waitlist tools (cloud-only) to search/update teams and manage user waitlist status.
    • Authenticated Waitlist page with a request form, validation, toasts, and rate-limited submission.
  • Refactor
    • Refreshed Admin SES Configurations page layout and headings; enhanced waitlisted user screen with a richer card UI.
  • Chores
    • Added optional environment variable for founder email notifications.
  • Documentation
    • Added guideline to prefer TRPC by default.

@vercel

vercelBot commented Sep 18, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
unsend-marketingReadyReadyPreviewCommentSep 18, 2025 11:01am

@coderabbitai

coderabbitaiBot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe title "add waitlist submission form" is concise and accurately describes a primary visible change in this PR — the new WaitListForm and waitlist submission flow — so it is directly related to the changeset. The branch also adds supporting backend and admin waitlist management, making the title slightly narrower than the full scope but still relevant and clear.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb5d0b and 817ae79.

📒 Files selected for processing (2)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Sep 18, 2025

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:817ae79
Status:⚡️ Build in progress...

View logs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)

84-86: Do not use Math.random() for verification tokens.

This is weak and predictable. Use crypto or NextAuth default.

Apply one of the fixes:

- async generateVerificationToken() {- return Math.random().toString(36).substring(2, 7).toLowerCase();- },

or

+ async generateVerificationToken() {+ const { randomBytes } = await import("crypto");+ return randomBytes(32).toString("hex");+ },
🧹 Nitpick comments (14)
AGENTS.md (1)

32-35: Fix wording and capitalization in new rule.

Use “tRPC” and correct grammar.

Apply this diff:

-## Rules--- Prefer to use trpc alway unless asked otherwise+## Rules++- Prefer to use tRPC by default unless asked otherwise.
apps/web/src/env.js (1)

53-53: Validate FOUNDER_EMAIL as an email address.

Type-level guard reduces misconfig at deploy time.

- FOUNDER_EMAIL: z.string().optional(),+ FOUNDER_EMAIL: z.string().email().optional(),
apps/web/src/app/wait-list/schema.ts (1)

8-24: Tighten domain validation.

Add a basic FQDN check and normalize casing.

 export const waitlistSubmissionSchema = z.object({
domain: z
.string({ required_error: "Domain is required" })
.trim()
+ .transform((v) => v.toLowerCase())
.min(1, "Domain is required")
- .max(255, "Domain must be 255 characters or fewer"),+ .max(255, "Domain must be 255 characters or fewer")+ .regex(+ /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/,+ "Enter a valid domain (e.g., example.com)"+ ),
emailTypes: z
.array(z.enum(WAITLIST_EMAIL_TYPES))
.min(1, "Select at least one email type"),
apps/web/src/server/api/trpc.ts (1)

119-125: Use FORBIDDEN for waitlisted users.

This is an authorization failure, not authentication; improves client handling.

- if (ctx.session.user.isWaitlisted) {- throw new TRPCError({ code: "UNAUTHORIZED" });- }+ if (ctx.session.user.isWaitlisted) {+ throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" });+ }
apps/web/src/app/wait-list/waitlist-form.tsx (1)

32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.

Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.

Add to schema.ts:

+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {+ transactional: "Transactional",+ marketing: "Marketing",+};

Then update this file:

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,- type WaitlistSubmissionInput,-} from "./schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ type WaitlistSubmissionInput,+ EMAIL_TYPE_LABEL,+} from "./schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};
apps/web/src/server/api/routers/waitlist.ts (2)

16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.

Use a single source of truth alongside WAITLIST_EMAIL_TYPES.

Example change here (after exporting from schema):

-import {- WAITLIST_EMAIL_TYPES,- waitlistSubmissionSchema,-} from "~/app/wait-list/schema";+import {+ WAITLIST_EMAIL_TYPES,+ waitlistSubmissionSchema,+ EMAIL_TYPE_LABEL,+} from "~/app/wait-list/schema";--const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = {- transactional: "Transactional",- marketing: "Marketing",-};

61-62: Clarify rate‑limit error message.

Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.

- message: "You have reached the waitlist request limit. Please try later.",+ message: "You have reached the waitlist request limit. Please try again in a few hours.",
apps/web/src/server/api/routers/admin.ts (2)

110-126: Use query for read‑only RPC.

findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.

- findUserByEmail: adminProcedure+ findUserByEmail: adminProcedure
.input(
z.object({
email: z
.string()
.email()
.transform((value) => value.toLowerCase()),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const user = await db.user.findUnique({
where: { email: input.email },
select: waitlistUserSelection,
});
return user ?? null;
}),

145-198: Use query for read‑only RPC.

findTeam is read‑only and should be a .query for consistency.

- findTeam: adminProcedure+ findTeam: adminProcedure
.input(
z.object({
query: z
.string({ required_error: "Search query is required" })
.trim()
.min(1, "Search query is required"),
}),
)
- .mutation(async ({ input }) => {+ .query(async ({ input }) => {
const query = input.query.trim();
…
return team ?? null;
}),
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

112-118: Redundant controlled prop.

{...field} already supplies value/onChange. Explicit value={field.value} is redundant and risks drift.

- <Input+ <Input
placeholder="user@example.com"
autoComplete="off"
- {...field}- value={field.value}+ {...field}
/>

44-49: Small UX: disable submit until form is valid and reflect busy state.

Prevents useless requests and improves a11y.

 const form = useForm<SearchInput>({
- resolver: zodResolver(searchSchema),+ resolver: zodResolver(searchSchema),+ mode: "onChange",
defaultValues: {
email: "",
},
});
- <Button type="submit" disabled={findUser.isPending}>+ <Button+ type="submit"+ disabled={!form.formState.isValid || findUser.isPending}+ aria-busy={findUser.isPending}+ >

Also applies to: 123-131

apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)

253-263: Numeric input jank and mobile keypad.

Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.

  • Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input+ <Input
type="number"
min={1}
max={10000}
+ inputMode="numeric"+ pattern="[0-9]*"
{...field}
- value={Number.isNaN(field.value) ? 1 : field.value}- onChange={(event) =>- field.onChange(Number(event.target.value))- }+ value={Number.isNaN(field.value) ? "" : field.value}+ onChange={(e) => {+ const v = e.target.value;+ field.onChange(v === "" ? Number.NaN : Number(v));+ }}+ onBlur={() => {+ if (Number.isNaN(field.value)) field.onChange(1);+ }}
disabled={updateTeam.isPending}
/>

Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.

Also applies to: 276-286


339-349: A11y: reflect busy state on submit.

Add aria-busy to communicate progress to AT.

- <Button type="submit" disabled={updateTeam.isPending}>+ <Button+ type="submit"+ disabled={updateTeam.isPending}+ aria-busy={updateTeam.isPending}+ >

167-171: Minor UX duplication.

You show both a toast ("No team found") and an inline empty-state message. Consider one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21cac61 and 3bb5d0b.

📒 Files selected for processing (15)
  • AGENTS.md (1 hunks)
  • apps/web/src/app/(dashboard)/admin/layout.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx (1 hunks)
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/page.tsx (1 hunks)
  • apps/web/src/app/wait-list/schema.ts (1 hunks)
  • apps/web/src/app/wait-list/waitlist-form.tsx (1 hunks)
  • apps/web/src/env.js (2 hunks)
  • apps/web/src/providers/next-auth.tsx (2 hunks)
  • apps/web/src/server/api/root.ts (2 hunks)
  • apps/web/src/server/api/routers/admin.ts (2 hunks)
  • apps/web/src/server/api/routers/waitlist.ts (1 hunks)
  • apps/web/src/server/api/trpc.ts (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier 3 via pnpm format for TypeScript and Markdown files

Files:

  • AGENTS.md
  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/env.js
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the / alias for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/wait-list/schema.ts
  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/server/auth.ts
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/server/api/routers/admin.ts
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
  • apps/web/src/server/api/root.ts
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/api/routers/waitlist.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/admin/page.tsx
  • apps/web/src/app/(dashboard)/admin/layout.tsx
  • apps/web/src/providers/next-auth.tsx
  • apps/web/src/app/wait-list/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/app/(dashboard)/admin/waitlist/page.tsx
  • apps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
  • AddSesConfiguration (16-40)
apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
  • SesConfigurations (17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
  • SettingsNavButton (7-39)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
  • adminProcedure (267-272)
apps/web/src/server/db.ts (1)
  • db (20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
  • getServerAuthSession (153-153)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
  • WaitListForm (37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
  • WAITLIST_EMAIL_TYPES (3-6)
  • WaitlistSubmissionInput (24-24)
  • waitlistSubmissionSchema (8-22)
packages/ui/src/spinner.tsx (1)
  • Spinner (4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/utils/common.ts (1)
  • isCloud (3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
  • AppRouter (40-40)
apps/web/src/server/service/team-service.ts (1)
  • updateTeam (104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
  • waitlistRouter (30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
  • WAITLIST_EMAIL_TYPES (3-6)
  • waitlistSubmissionSchema (8-22)
apps/web/src/server/api/trpc.ts (2)
  • createTRPCRouter (82-82)
  • authedProcedure (99-109)
apps/web/src/env.js (2)
  • env (5-134)
  • env (5-134)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/redis.ts (1)
  • getRedis (6-13)
apps/web/src/server/mailer.ts (1)
  • sendMail (72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)

132-141: Waitlist gating logic change looks good.

Behavior aligns with protectedProcedure waitlist checks.

Please verify onboarding flows for:

  • Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
  • Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.

91-93: Typo in error message.

“atleast” → “at least”.
[raise_nitpick_refactor]

- throw new Error("No auth providers found, need atleast one");+ throw new Error("No auth providers found, need at least one");
apps/web/src/server/api/trpc.ts (1)

99-109: Auth middleware LGTM.

Session presence check and context shaping look fine.

apps/web/src/server/api/root.ts (1)

15-16: Router wiring LGTM.

waitlistRouter import and exposure are correct.

Also applies to: 36-36

apps/web/src/app/(dashboard)/admin/page.tsx (1)

6-14: Admin SES page refactor LGTM.

Header/actions layout and component split are clean.

apps/web/src/app/wait-list/page.tsx (1)

3-15: Waitlist page gating and UI LGTM.

Server-side redirect and form embedding are correct.

Also applies to: 17-33

apps/web/src/env.js (1)

107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.

apps/web/src/app/(dashboard)/admin/layout.tsx (2)

11-29: LGTM: Admin layout and nav structure look good.


21-25: No change needed — isCloud already returns a boolean.

apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.

apps/web/src/providers/next-auth.tsx (1)

41-56: Nice UX for waitlisted users; clear CTA and consistent styling.

apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)

30-33: Good: Strong typing via AppRouter + inferRouterOutputs.

Nice use of TRPC types to derive TeamAdmin; avoids drift.

Also applies to: 43-45


85-99: Mutation handling and form sync look correct.

Search/update flows, toasts, and reset-on-success are coherent.

Also applies to: 100-114

apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)

160-166: Type safety: createdAt conversion.new Date(userResult.createdAt) safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.


51-79: Mutation flow OK — server returns isWaitlisted and createdAt.

Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).

Comment on lines +92 to +99
if (!isCloud()) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.

isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.

Apply in apps/web/src/utils/common.ts:

+import { env } from "~/env";-export function isCloud() {- return env.NEXT_PUBLIC_IS_CLOUD;-}+export function isCloud(): boolean {+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";+}

Optionally, keep this file unchanged and do a strict check here:

- if (!isCloud()) {+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.

} from "@usesend/ui/src/form";
import { Input } from "@usesend/ui/src/input";
import { Textarea } from "@usesend/ui/src/textarea";
import Spinner from "@usesend/ui/src/spinner";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export.

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";

Quick check:


🏁 Script executed:

#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/ui

Length of output: 307


Fix incorrect default import for Spinner (build-time error).

packages/ui exports Spinner as a named export — change the import to a named import.

File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)

Apply:

-import Spinner from "@usesend/ui/src/spinner";+import { Spinner } from "@usesend/ui/src/spinner";
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
importSpinnerfrom"@usesend/ui/src/spinner";
import{Spinner}from"@usesend/ui/src/spinner";
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.

@KMKoushik
KMKoushik merged commit 87c772d into mainSep 18, 2025
2 of 5 checks passed
@KMKoushik
KMKoushik deleted the km/2025-09-18-waitlist-form branch September 18, 2025 11:01
@coderabbitaicoderabbitaiBot mentioned this pull request Nov 30, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@KMKoushik