Redesign mobile blocking flow and modernize email templates using React Email components#362
Conversation
…ail reminder flow, and moderniz
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 55 minutes and 23 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR introduces a mobile reminder email feature. It adds an API endpoint to send desktop reminder emails to mobile users, creates a new email template, detects mobile devices, and updates the mobile warning component to capture and submit user emails instead of displaying a static warning. Changes
Sequence DiagramsequenceDiagram
actor Mobile as Mobile Client
participant MW as MobileWarning Component
participant API as POST /api/mobile-reminder
participant Resend as Resend Email Service
Mobile->>MW: Opens page on mobile device
MW->>MW: useIsMobileDevice detects mobile
MW->>Mobile: Renders email capture form
Mobile->>MW: Enters email & submits
MW->>MW: Validates email via regex
alt Validation fails
MW->>Mobile: Show error toast
else Validation passes
MW->>API: POST {email}
API->>API: Normalize & validate email
API->>Resend: Send DesktopReminderEmail
Resend-->>API: Email sent or error
alt Resend succeeds
API-->>MW: {success: true}
MW->>MW: Show success message
MW->>Mobile: Display confirmation
else Resend fails
API-->>MW: {error: "Failed to send email"}
MW->>Mobile: Show error toast
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR overhauls the mobile blocking UX with two distinct flows (device-based landing page with email capture vs. small-window warning), adds a
Confidence Score: 4/5Safe to merge once the unauthenticated email endpoint is rate-limited or gated behind auth. One P1 finding: the src/app/api/mobile-reminder/route.ts requires rate limiting or authentication before this ships.
|
| Filename | Overview |
|---|---|
| src/app/api/mobile-reminder/route.ts | New unauthenticated POST endpoint that sends a reminder email via Resend; missing rate limiting and authentication allows email abuse via ThinkEx's sending infrastructure. |
| src/components/email/desktop-reminder-email.tsx | New React Email component for desktop reminder; clean implementation with minor missing lang attribute on <Html>. |
| src/components/email/invite-email.tsx | Migrated from raw HTML tables to React Email primitives; notable design shift from dark (#111113) to light (#f6f9fc) theme and lang attribute removed from <Html>. |
| src/components/ui/MobileWarning.tsx | Refactored into two distinct blocking flows (device-based landing page vs. small-window warning) with email submission form; validation and error handling look solid. |
| src/hooks/ui/use-mobile-device.ts | New hook using navigator.userAgent and maxTouchPoints for iPad detection; correctly deferred to useEffect to avoid SSR issues. |
Reviews (1): Last reviewed commit: "Redesign mobile blocking screen with dev..." | Re-trigger Greptile
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const { email } = await request.json(); | ||
|
|
||
| if (!email || typeof email !== "string") { | ||
| return NextResponse.json({ error: "Email is required" }, { status: 400 }); | ||
| } | ||
|
|
||
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||
| if (!emailRegex.test(email.trim())) { | ||
| return NextResponse.json( | ||
| { error: "Invalid email address" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const normalizedEmail = email.trim().toLowerCase(); | ||
|
|
||
| const { error } = await resend.emails.send({ | ||
| from: "ThinkEx <hello@thinkex.app>", | ||
| to: [normalizedEmail], | ||
| subject: "Your ThinkEx link — open on desktop", | ||
| react: DesktopReminderEmail(), | ||
| }); |
There was a problem hiding this comment.
Unauthenticated endpoint allows email abuse
This route accepts any email address and triggers a Resend email send with no authentication check, no rate limiting, and no verification that the requester owns the target address. Anyone who discovers the endpoint can call it in a tight loop to spam arbitrary inboxes via ThinkEx's sending infrastructure, exhaust the Resend quota, and risk getting the domain flagged for abuse.
At minimum, add per-IP rate limiting (e.g. with @upstash/ratelimit or a simple in-memory counter) and optionally require the caller to be authenticated before sending.
| backgroundColor: "#f6f9fc", | ||
| fontFamily: | ||
| 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', | ||
| margin: 0, | ||
| padding: 0, | ||
| }} | ||
| > | ||
| <Preview>{previewText}</Preview> |
There was a problem hiding this comment.
Invite email lost its dark theme
The original template used a dark background (#111113) with white text — consistent with ThinkEx's dark-themed UI. The new version switches to a light background (#f6f9fc) which may feel inconsistent to users who received previous invite emails or who associate the product with a dark aesthetic. Was this intentional? If so, it's worth confirming with design.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
|
||
| export function DesktopReminderEmail() { | ||
| return ( | ||
| <Html> |
There was a problem hiding this comment.
Missing
lang attribute on <Html>
Both new/updated email templates (DesktopReminderEmail and InviteEmailTemplate) omit the lang attribute that was present on the old invite template (<Html lang="en">). Screen readers and translation engines rely on this for correct language identification.
| <Html> | |
| <Html lang="en"> |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/components/email/invite-email.tsx (1)
112-121: Restore wrapping on the fallback URL.
workspaceUrlcan include long invite tokens fromsrc/app/api/workspaces/[id]/collaborators/route.ts, Lines 283-303. Rendering it as a single unbroken string with nowordBreak/overflowWrapstyle can blow out the card in some mail clients, which makes the non-button fallback less reliable.Proposed fix
<Text style={{ fontSize: "14px", lineHeight: "24px", color: "#525f7f" }} > or copy and paste this link into your browser:{" "} <Link href={workspaceUrl} - style={{ color: "#556cd6", textDecoration: "none" }} + style={{ + color: "#556cd6", + textDecoration: "none", + wordBreak: "break-all", + overflowWrap: "anywhere", + }} > {workspaceUrl} </Link> </Text>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/email/invite-email.tsx` around lines 112 - 121, The fallback invite URL (workspaceUrl) is rendered unbroken in invite-email.tsx inside the Link/Text block which can overflow mail card layouts; update the Link (or its parent Text) rendering of workspaceUrl to allow wrapping by adding CSS like wordBreak: "break-word" and/or overflowWrap: "anywhere" (or equivalent inline style properties) on the Link element that renders workspaceUrl so long tokens wrap correctly in email clients.src/components/ui/MobileWarning.tsx (1)
12-12: Share this validator with the API route.The same email regex now exists here and in
src/app/api/mobile-reminder/route.ts, so any later tweak can make the UI and server disagree on what is valid. A small sharednormalizeEmail/isValidEmailhelper would keep the behavior aligned.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/MobileWarning.tsx` at line 12, Replace the duplicated EMAIL_REGEX by extracting the validation logic into a small shared helper module that exports normalizeEmail and isValidEmail; remove the local EMAIL_REGEX constant in MobileWarning (symbol EMAIL_REGEX) and import isValidEmail/normalizeEmail there, and update the API route (mobile-reminder route) to import and use the same isValidEmail/normalizeEmail functions so both client and server rely on the identical regex/normalization; ensure the helper exports are named normalizeEmail and isValidEmail and update any call sites to use those names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/api/mobile-reminder/route.ts`:
- Around line 7-40: The POST handler currently calls resend.emails.send directly
and must enforce abuse protection: before calling resend.emails.send (inside
POST) add IP-based rate limiting using the request IP (e.g., X-Forwarded-For or
connection remoteAddr) with a shared store (Redis or in-memory with TTL) to cap
requests per IP (return 429 when exceeded), implement lightweight bot friction
such as requiring a short-lived token/nonce or a simple honeypot/body field or
captcha verification (validate that token/nonce server-side) before proceeding,
and dedupe rapid repeats by checking a recent-sends key for normalizedEmail+IP
with a short TTL to block duplicate send attempts (return 429 or 409); ensure
all checks occur before invoking resend.emails.send and include clear error
responses for rate-limit and bot-friction failures.
- Around line 15-23: Before running the emailRegex test, guard against very
large attacker-controlled input by trimming and checking length first: trim the
incoming email, reject if its length exceeds a reasonable max (e.g. 254) with
the same NextResponse.json 400 pattern, then proceed to lower-case/normalize
into normalizedEmail and run emailRegex.test; update the code paths around
email.trim(), emailRegex, and normalizedEmail to use the trimmed value and to
return early on over-long inputs.
---
Nitpick comments:
In `@src/components/email/invite-email.tsx`:
- Around line 112-121: The fallback invite URL (workspaceUrl) is rendered
unbroken in invite-email.tsx inside the Link/Text block which can overflow mail
card layouts; update the Link (or its parent Text) rendering of workspaceUrl to
allow wrapping by adding CSS like wordBreak: "break-word" and/or overflowWrap:
"anywhere" (or equivalent inline style properties) on the Link element that
renders workspaceUrl so long tokens wrap correctly in email clients.
In `@src/components/ui/MobileWarning.tsx`:
- Line 12: Replace the duplicated EMAIL_REGEX by extracting the validation logic
into a small shared helper module that exports normalizeEmail and isValidEmail;
remove the local EMAIL_REGEX constant in MobileWarning (symbol EMAIL_REGEX) and
import isValidEmail/normalizeEmail there, and update the API route
(mobile-reminder route) to import and use the same isValidEmail/normalizeEmail
functions so both client and server rely on the identical regex/normalization;
ensure the helper exports are named normalizeEmail and isValidEmail and update
any call sites to use those names.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2d2249f7-20dd-418c-af36-5c5e1d2652c6
📒 Files selected for processing (5)
src/app/api/mobile-reminder/route.tssrc/components/email/desktop-reminder-email.tsxsrc/components/email/invite-email.tsxsrc/components/ui/MobileWarning.tsxsrc/hooks/ui/use-mobile-device.ts
There was a problem hiding this comment.
4 issues found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/components/email/invite-email.tsx">
<violation number="1" location="src/components/email/invite-email.tsx:118">
P2: The fallback URL link no longer enforces wrapping, so long invite URLs can overflow and break email layout in narrow clients.</violation>
</file>
<file name="src/components/email/desktop-reminder-email.tsx">
<violation number="1" location="src/components/email/desktop-reminder-email.tsx:18">
P3: Missing `lang` attribute on `<Html>`. The previous invite template specified `<Html lang="en">` for accessibility. Both new/updated email templates should include it — screen readers and translation engines rely on it for correct language identification.</violation>
</file>
<file name="src/app/api/mobile-reminder/route.ts">
<violation number="1" location="src/app/api/mobile-reminder/route.ts:9">
P2: Invalid JSON requests are treated as server errors; malformed bodies should return 400 instead of 500.</violation>
<violation number="2" location="src/app/api/mobile-reminder/route.ts:25">
P1: This public email-sending endpoint lacks anti-abuse controls (auth/rate-limit/challenge), so it can be abused for spam.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| const normalizedEmail = email.trim().toLowerCase(); | ||
|
|
||
| const { error } = await resend.emails.send({ |
There was a problem hiding this comment.
P1: This public email-sending endpoint lacks anti-abuse controls (auth/rate-limit/challenge), so it can be abused for spam.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/api/mobile-reminder/route.ts, line 25:
<comment>This public email-sending endpoint lacks anti-abuse controls (auth/rate-limit/challenge), so it can be abused for spam.</comment>
<file context>
@@ -0,0 +1,48 @@
+
+ const normalizedEmail = email.trim().toLowerCase();
+
+ const { error } = await resend.emails.send({
+ from: "ThinkEx <hello@thinkex.app>",
+ to: [normalizedEmail],
</file context>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/app/api/mobile-reminder/route.ts">
<violation number="1" location="src/app/api/mobile-reminder/route.ts:7">
P2: The new in-memory rate-limit map never evicts expired keys, which can grow memory unbounded under many unique emails/IPs.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/app/api/mobile-reminder/route.ts">
<violation number="1" location="src/app/api/mobile-reminder/route.ts:45">
P2: Guard that the parsed JSON body is an object before destructuring `email`; otherwise malformed JSON values can trigger a 500 instead of a 400.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| ); | ||
| } | ||
|
|
||
| const { email } = body as { email?: unknown }; |
There was a problem hiding this comment.
P2: Guard that the parsed JSON body is an object before destructuring email; otherwise malformed JSON values can trigger a 500 instead of a 400.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/api/mobile-reminder/route.ts, line 45:
<comment>Guard that the parsed JSON body is an object before destructuring `email`; otherwise malformed JSON values can trigger a 500 instead of a 400.</comment>
<file context>
@@ -25,21 +32,38 @@ function isRateLimited(key: string, maxAttempts: number): boolean {
+ );
+ }
+
+ const { email } = body as { email?: unknown };
if (!email || typeof email !== "string") {
</file context>
| const { email } = body as { email?: unknown }; | |
| if (!body || typeof body !== "object") { | |
| return NextResponse.json( | |
| { error: "Invalid request body" }, | |
| { status: 400 }, | |
| ); | |
| } | |
| const { email } = body as { email?: unknown }; |
Mobile blocking UI overhaul, email templates modernization for React Email, and new mobile-reminder API endpoint. This PR introduces a robust mobile device detection hook via userAgent, replaces the old generic mobile warning with two distinct blocking flows (device-based landing page vs small-window warning), adds the desktop reminder email service implementation, and updates the invite email component from raw HTML tables to React Email primitives.
Summary by CodeRabbit
Release Notes