Skip to content

Redesign mobile blocking flow and modernize email templates using React Email components#362

Merged
urjitc merged 3 commits into
mainfrom
capy/mobile-blocking-flow-redesign
Apr 15, 2026
Merged

Redesign mobile blocking flow and modernize email templates using React Email components#362
urjitc merged 3 commits into
mainfrom
capy/mobile-blocking-flow-redesign

Conversation

@urjitc

@urjitc urjitc commented Apr 14, 2026

Copy link
Copy Markdown
Member

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.

Open in Capy ENG-40 · 5.4

Summary by CodeRabbit

Release Notes

  • New Features
    • Users on mobile devices can now enter their email to receive a desktop reminder and continue using the app on larger screens
    • Improved email template styling for better visual presentation across devices

@vercel

vercel Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
thinkex Ready Ready Preview, Comment Apr 15, 2026 2:29am

Request Review

@urjitc urjitc added the capy Generated by capy.ai label Apr 14, 2026 — with Capy AI
@github-project-automation github-project-automation Bot moved this to Backlog in Dev Board Apr 14, 2026
@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@urjitc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 23 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6e5ce0a-2790-42fe-90ac-25de96f8079a

📥 Commits

Reviewing files that changed from the base of the PR and between e43b3c2 and fbf2c95.

📒 Files selected for processing (3)
  • src/app/api/mobile-reminder/route.ts
  • src/components/email/desktop-reminder-email.tsx
  • src/components/email/invite-email.tsx
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Mobile Reminder API
src/app/api/mobile-reminder/route.ts
New POST route that validates email input via regex, normalizes it (trim/lowercase), and sends a desktop reminder email via Resend. Returns appropriate error responses for invalid/missing emails and server errors.
Email Templates
src/components/email/desktop-reminder-email.tsx, src/components/email/invite-email.tsx
New DesktopReminderEmail component with React Email layout; refactored InviteEmailTemplate from table-based HTML to React-Email components, updating styling with bordered white card, rounded corners, and adjusted preview/footer text.
Mobile Device Detection & Warning UI
src/hooks/ui/use-mobile-device.ts, src/components/ui/MobileWarning.tsx
New hook useIsMobileDevice that detects mobile/tablet via user-agent and touch-point heuristics. MobileWarning now conditionally renders email capture form (MobileLandingPage) on mobile or screen-size warning (SmallScreenWarning), with form submission to the reminder API and toast-based feedback.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • feat(email): enhance email ui #176: Both PRs modify src/components/email/invite-email.tsx, with this PR refactoring it to React-Email components while the related PR enhances email UI and adds permission-level/workspace-color support.

Poem

🐰 A mobile reminder hops through the wire,
Email validation, oh what we require!
Resend takes flight with desktop delight,
Users on phones now get it just right! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the two main changes: mobile blocking flow redesign and email template modernization using React Email components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch capy/mobile-blocking-flow-redesign

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.

❤️ Share

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

Comment thread src/app/api/mobile-reminder/route.ts Fixed

@capy-ai capy-ai Bot 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.

Added 1 comment

Comment thread src/app/api/mobile-reminder/route.ts
@greptile-apps

greptile-apps Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR overhauls the mobile blocking UX with two distinct flows (device-based landing page with email capture vs. small-window warning), adds a /api/mobile-reminder endpoint to send desktop-link emails via Resend, and modernizes both email templates to use React Email primitives.

  • P1 — email abuse vector: The /api/mobile-reminder route is unauthenticated with no rate limiting; anyone can call it to send emails to arbitrary addresses through ThinkEx's Resend account.

Confidence Score: 4/5

Safe to merge once the unauthenticated email endpoint is rate-limited or gated behind auth.

One P1 finding: the /api/mobile-reminder route has no rate limiting or auth, making it an open email-send relay that can be abused. The rest of the changes (UI components, email templates, hook) are well-structured with no blocking issues.

src/app/api/mobile-reminder/route.ts requires rate limiting or authentication before this ships.

Security Review

  • Email abuse / sender reputation risk (src/app/api/mobile-reminder/route.ts): The POST endpoint is publicly accessible with no authentication and no rate limiting. Any actor can call it in a loop to send unsolicited emails to arbitrary addresses using ThinkEx's Resend credentials, risking quota exhaustion and domain blacklisting.

Important Files Changed

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.

Fix All in Cursor

Reviews (1): Last reviewed commit: "Redesign mobile blocking screen with dev..." | Re-trigger Greptile

Comment on lines +7 to +30
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(),
});

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.

P1 security 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.

Fix in Cursor

Comment on lines +37 to +44
backgroundColor: "#f6f9fc",
fontFamily:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
margin: 0,
padding: 0,
}}
>
<Preview>{previewText}</Preview>

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.

P2 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!

Fix in Cursor


export function DesktopReminderEmail() {
return (
<Html>

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.

P2 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.

Suggested change
<Html>
<Html lang="en">

Fix in Cursor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/components/email/invite-email.tsx (1)

112-121: Restore wrapping on the fallback URL.

workspaceUrl can include long invite tokens from src/app/api/workspaces/[id]/collaborators/route.ts, Lines 283-303. Rendering it as a single unbroken string with no wordBreak/overflowWrap style 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 shared normalizeEmail/isValidEmail helper 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

📥 Commits

Reviewing files that changed from the base of the PR and between f522622 and e43b3c2.

📒 Files selected for processing (5)
  • src/app/api/mobile-reminder/route.ts
  • src/components/email/desktop-reminder-email.tsx
  • src/components/email/invite-email.tsx
  • src/components/ui/MobileWarning.tsx
  • src/hooks/ui/use-mobile-device.ts

Comment thread src/app/api/mobile-reminder/route.ts
Comment thread src/app/api/mobile-reminder/route.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

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({

@cubic-dev-ai cubic-dev-ai Bot Apr 14, 2026

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.

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>
Fix with Cubic

Comment thread src/components/email/invite-email.tsx Outdated
Comment thread src/app/api/mobile-reminder/route.ts Outdated
Comment thread src/components/email/desktop-reminder-email.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

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.

Comment thread src/app/api/mobile-reminder/route.ts

@cubic-dev-ai cubic-dev-ai Bot 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.

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 };

@cubic-dev-ai cubic-dev-ai Bot Apr 15, 2026

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.

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>
Suggested change
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 };
Fix with Cubic

@urjitc
urjitc merged commit a226dfb into main Apr 15, 2026
8 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Apr 15, 2026
@urjitc
urjitc deleted the capy/mobile-blocking-flow-redesign branch June 29, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capy Generated by capy.ai

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants