Skip to content

integrated the support section using plain#313

Open
kshitij2040 wants to merge 2 commits into
mainfrom
update/plain_support
Open

integrated the support section using plain#313
kshitij2040 wants to merge 2 commits into
mainfrom
update/plain_support

Conversation

@kshitij2040

@kshitij2040 kshitij2040 commented Feb 19, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Integrated a client-side support chat and added a visible "Chat with Support" entry in user menus.
    • Added an email verification resend flow with user-facing toasts for success/error.
  • UI Improvements

    • Simplified sidebar by removing the hover-based Support popover and using the new chat entry.
  • Style

    • Non-functional formatting and spacing cleanup in global styles (no visual or color changes).

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a Plain chat provider (injects styles and external script) and mounts it in LayoutProviders; replaces the sidebar hover support popover with a direct “Chat with Support” action in the user menu; adds an email verification resend flow in the user menu; and applies minor formatting/selector edits to globals.css.

Changes

Cohort / File(s) Summary
Plain Chat Provider
app/plain-chat.tsx, providers/LayoutProviders.tsx
New default-exported PlainChatProvider component that injects Light-mode CSS and appends https://chat.cdn-plain.com/index.js; LayoutProviders now renders <PlainChatProvider />.
User Menu / Verification
components/Layouts/minors/nav-user.tsx
Added emailVerified?: boolean to user type; added resend verification flow (fetch Firebase ID token → POST /api/resend-verification), sonner toasts, loading state; replaced Support link with “Chat with Support” that opens Plain chat; updated icons.
Sidebar Simplification
components/Layouts/Sidebar.tsx
Removed hover-triggered Support popover and related state/refs/handlers; eliminated Popover import and JSX for that popover.
CSS formatting & selector tweak
app/globals.css
Whitespace/comment reflows and minor formatting; selector changed from .markdown-content > * to .markdown-content>* (spacing removed); no semantic style behavior changes.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (NavUser)
    participant Nav as NavUser Component
    participant Firebase as Firebase Auth
    participant Backend as Backend API
    participant Toast as Toast Notification

    User->>Nav: Click "Resend Verification"
    Nav->>Firebase: requestIdToken()
    Firebase-->>Nav: idToken
    Nav->>Backend: POST /api/resend-verification (idToken, email/githubId)
    Backend-->>Nav: success / error
    Nav->>Toast: show success/error toast
    Nav->>Nav: reset isSendingVerification
Loading
sequenceDiagram
    participant Browser as Browser Client
    participant Provider as PlainChatProvider
    participant CDN as Plain CDN (chat.cdn-plain.com)
    participant Window as window.Plain

    Browser->>Provider: Mount (LayoutProviders)
    Provider->>Provider: useEffect — inject CSS variables & styles
    Provider->>CDN: append script (index.js)
    CDN-->>Window: script loaded
    Provider->>Window: initialize Plain (appId, hideLauncher, theme=light, links)
    Window-->>Browser: chat widget ready
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • yashkrishan
  • ASCE-D
  • nndn

Poem

🐇 I hid a script beneath the light,
I set the colors, snug and bright.
A toast went up for mails sent true,
Sidebar trimmed to make room too.
Hoppity cheers — a rabbit’s byte ✨

🚥 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 'integrated the support section using plain' directly describes the main changes: integrating Plain chat for support functionality across multiple components.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch update/plain_support

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.

@coderabbitai coderabbitai 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.

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)
providers/LayoutProviders.tsx (1)

46-46: 🛠️ Refactor suggestion | 🟠 Major

Pre-existing: new QueryClient() is instantiated on every render.

This isn't introduced by this PR, but new QueryClient() inside the render body creates a fresh client each render, defeating React Query's caching. It should be hoisted to module scope or wrapped in useState/useRef.

Suggested fix
+const queryClient = new QueryClient();
+
 const LayoutProviders = ({ children }: { children: React.ReactNode }) => {
   return (
     <PostHogProvider client={posthog}>
-      <QueryClientProvider client={new QueryClient()}>
+      <QueryClientProvider client={queryClient}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@providers/LayoutProviders.tsx` at line 46, The QueryClient is being
re-created on every render because new QueryClient() is passed directly into the
JSX; move its instantiation out of the render so React Query can preserve cache.
Fix by creating a single QueryClient instance (hoist to module scope or
initialize once with useRef/useState) and pass that stable instance into
QueryClientProvider (referencing QueryClientProvider and QueryClient in
LayoutProviders.tsx); ensure the provider uses that persistent variable instead
of calling new QueryClient() inside the component body.
🧹 Nitpick comments (5)
app/plain-chat.tsx (3)

64-64: Hardcoded appId — move to an environment variable.

Embedding the Plain app ID directly in source makes it harder to manage across environments (staging vs. production) and exposes it in the client bundle regardless. Use process.env.NEXT_PUBLIC_PLAIN_APP_ID instead.

-                        appId: "liveChatApp_01KHRDQSB5WJHKCW4ZE3GFH7K5",
+                        appId: process.env.NEXT_PUBLIC_PLAIN_APP_ID!,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` at line 64, Replace the hardcoded Plain appId with a
public environment variable: use process.env.NEXT_PUBLIC_PLAIN_APP_ID wherever
the appId property is set in plain-chat.tsx (the object with the appId key) and
remove the literal string; also add a clear fallback or runtime check that
throws/logs a helpful error if NEXT_PUBLIC_PLAIN_APP_ID is undefined so
deployments fail fast in missing-config scenarios.

55-89: script.async = false blocks HTML parsing — prefer async = true for a chat widget.

Setting async = false makes the browser download and execute the script synchronously relative to other scripts, which can delay page interactivity. A chat widget is non-critical and should load asynchronously.

Also, inconsistent DOM API: line 53 uses document.head.appendChild(style) but line 88 uses d.getElementsByTagName("head")[0].appendChild(script). Simplify for consistency.

Proposed fix
         const script = d.createElement("script");
         script.id = "plain-chat-script";
-        script.async = false;
+        script.async = true;
         script.onload = function () {
             ...
         };
         script.src = "https://chat.cdn-plain.com/index.js";
-        d.getElementsByTagName("head")[0].appendChild(script);
+        d.head.appendChild(script);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` around lines 55 - 89, Change the chat script to load
asynchronously by setting script.async = true instead of false, and append it
using the same DOM API used elsewhere for consistency (use
document.head.appendChild(script) rather than
d.getElementsByTagName("head")[0].appendChild(script)). Keep the existing onload
handler that checks window.Plain and calls window.Plain.init with the current
options; only modify the async flag and the append call to match document.head
usage.

60-63: Replace @ts-ignore with a proper type declaration.

Multiple @ts-ignore comments suppress type checking entirely. A minimal ambient declaration is cleaner and catches actual type errors:

// In a global.d.ts or at the top of this file:
declare global {
  interface Window {
    Plain?: {
      init: (config: Record<string, unknown>) => void;
      open?: () => void;
    };
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` around lines 60 - 63, Remove the two // `@ts-ignore` lines
and add an ambient type declaration for window.Plain (so TypeScript knows about
Plain.init and Plain.open) — e.g., add a declare global { interface Window {
Plain?: { init: (config: Record<string, unknown>) => void; open?: () => void; };
} } either in a global.d.ts or at the top of this file, then call
window.Plain.init(...) and window.Plain.open() without using // `@ts-ignore`.
components/Layouts/minors/nav-user.tsx (2)

76-85: API call lacks a timeout — could hang indefinitely on a slow/unreachable backend.

The fetch call to /api/v1/account/resend-verification has no AbortController / timeout. If the backend is slow or unreachable, the user sees an indefinite loading state (isSendingVerification stays true).

Suggested fix
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000);
+
     const response = await fetch(
       `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:8000'}/api/v1/account/resend-verification`,
       {
         method: 'POST',
         headers: {
           'Authorization': `Bearer ${token}`,
           'Content-Type': 'application/json',
         },
+        signal: controller.signal,
       }
     );
+    clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/minors/nav-user.tsx` around lines 76 - 85, The fetch call
that assigns to response when posting to `/api/v1/account/resend-verification`
can hang; wrap this request in an AbortController with a timeout (e.g., 5s) so
the request is aborted if it exceeds the limit, pass controller.signal into
fetch, and clear the timeout on success; update the error handling around the
response/token logic (and ensure isSendingVerification is set to false in both
the catch and abort branches) so the UI doesn't remain loading after an aborted
or timed-out request.

174-180: Chat with Support integration looks good.

The handleOpenPlainChat correctly checks for window.Plain before calling open?.() with optional chaining. This gracefully handles the case where the Plain script hasn't loaded yet, though the user gets no feedback in that scenario.

Consider adding a fallback toast when window.Plain is unavailable:

Suggested improvement
   const handleOpenPlainChat = () => {
     // `@ts-ignore`
     if (window.Plain) {
       // `@ts-ignore`
       window.Plain.open?.();
+    } else {
+      toast.error("Chat support is loading. Please try again in a moment.");
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/minors/nav-user.tsx` around lines 174 - 180, The dropdown
item calls handleOpenPlainChat which currently checks window.Plain and
optionally invokes open?.() but provides no user feedback if Plain isn't loaded;
update handleOpenPlainChat to show a fallback toast/notification when
window.Plain is undefined (e.g., use your existing toast/snackbar utility) so
users know support chat isn't available, and ensure the DropdownMenuItem still
calls handleOpenPlainChat; reference the handleOpenPlainChat function and
window.Plain check and add a brief user-facing message via the toast path when
Plain is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/plain-chat.tsx`:
- Around line 28-32: Remove the overly broad attribute selector
`[class*="plain"], [class*="plain"] * { ... }` from the CSS in plain-chat.tsx;
instead target the chat UI with the existing specific selectors such as
`[data-plain-chat]` and `.plain-chat-container` (or add a `.plain-chat-*` class
prefix) and keep the background-color/color rules scoped there without
`!important`, ensuring you only affect elements intended to be plain chat.

In `@components/Layouts/minors/nav-user.tsx`:
- Around line 8-11: The component imports CheckCircle2, AlertCircle, and Mail
and defines handleResendVerification and an emailVerified prop that are unused
because Sidebar.tsx doesn't pass emailVerified; remove dead code or wire it up:
either delete the unused imports (CheckCircle2, AlertCircle, Mail), remove the
entire handleResendVerification function and the emailVerified prop usage in
this component, or alternatively add a menu item that calls
handleResendVerification and update the parent (Sidebar.tsx) to pass a real
emailVerified prop; ensure you update any prop types or interfaces referencing
emailVerified and remove any unreachable logic that checks emailVerified if you
choose deletion.

---

Outside diff comments:
In `@providers/LayoutProviders.tsx`:
- Line 46: The QueryClient is being re-created on every render because new
QueryClient() is passed directly into the JSX; move its instantiation out of the
render so React Query can preserve cache. Fix by creating a single QueryClient
instance (hoist to module scope or initialize once with useRef/useState) and
pass that stable instance into QueryClientProvider (referencing
QueryClientProvider and QueryClient in LayoutProviders.tsx); ensure the provider
uses that persistent variable instead of calling new QueryClient() inside the
component body.

---

Nitpick comments:
In `@app/plain-chat.tsx`:
- Line 64: Replace the hardcoded Plain appId with a public environment variable:
use process.env.NEXT_PUBLIC_PLAIN_APP_ID wherever the appId property is set in
plain-chat.tsx (the object with the appId key) and remove the literal string;
also add a clear fallback or runtime check that throws/logs a helpful error if
NEXT_PUBLIC_PLAIN_APP_ID is undefined so deployments fail fast in missing-config
scenarios.
- Around line 55-89: Change the chat script to load asynchronously by setting
script.async = true instead of false, and append it using the same DOM API used
elsewhere for consistency (use document.head.appendChild(script) rather than
d.getElementsByTagName("head")[0].appendChild(script)). Keep the existing onload
handler that checks window.Plain and calls window.Plain.init with the current
options; only modify the async flag and the append call to match document.head
usage.
- Around line 60-63: Remove the two // `@ts-ignore` lines and add an ambient type
declaration for window.Plain (so TypeScript knows about Plain.init and
Plain.open) — e.g., add a declare global { interface Window { Plain?: { init:
(config: Record<string, unknown>) => void; open?: () => void; }; } } either in a
global.d.ts or at the top of this file, then call window.Plain.init(...) and
window.Plain.open() without using // `@ts-ignore`.

In `@components/Layouts/minors/nav-user.tsx`:
- Around line 76-85: The fetch call that assigns to response when posting to
`/api/v1/account/resend-verification` can hang; wrap this request in an
AbortController with a timeout (e.g., 5s) so the request is aborted if it
exceeds the limit, pass controller.signal into fetch, and clear the timeout on
success; update the error handling around the response/token logic (and ensure
isSendingVerification is set to false in both the catch and abort branches) so
the UI doesn't remain loading after an aborted or timed-out request.
- Around line 174-180: The dropdown item calls handleOpenPlainChat which
currently checks window.Plain and optionally invokes open?.() but provides no
user feedback if Plain isn't loaded; update handleOpenPlainChat to show a
fallback toast/notification when window.Plain is undefined (e.g., use your
existing toast/snackbar utility) so users know support chat isn't available, and
ensure the DropdownMenuItem still calls handleOpenPlainChat; reference the
handleOpenPlainChat function and window.Plain check and add a brief user-facing
message via the toast path when Plain is missing.

Comment thread app/plain-chat.tsx Outdated
Comment thread components/Layouts/minors/nav-user.tsx
@github-actions

Copy link
Copy Markdown

Cloud Run service deployed: https://pr-update-plain-support-cj6r7x3fpa-uc.a.run.app

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
components/Layouts/minors/nav-user.tsx (1)

151-151: ⚠️ Potential issue | 🟡 Minor

Hardcoded fallback "CN" — should use user initials like the trigger avatar does.

Line 126 uses user.name?.charAt(0) || "U" for the fallback, but the dropdown label avatar on line 151 is hardcoded to "CN". This looks like a copy-paste oversight.

Suggested fix
-                  <AvatarFallback className="rounded-lg">CN</AvatarFallback>
+                  <AvatarFallback className="rounded-lg">
+                    {user.name?.charAt(0) || "U"}
+                  </AvatarFallback>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/minors/nav-user.tsx` at line 151, Replace the hardcoded
AvatarFallback text "CN" with the same user-initials logic used by the trigger
avatar (e.g. use user.name?.charAt(0) || "U" or a shared getInitials(user.name)
helper); update the AvatarFallback element (symbol: AvatarFallback) inside the
dropdown so it derives initials from user.name rather than the literal "CN" to
keep both avatars consistent.
🧹 Nitpick comments (1)
components/Layouts/minors/nav-user.tsx (1)

34-35: Import order: React imports should come first.

Per coding guidelines, React/Next imports should be ordered before everything else. useState from "react" (line 35) and useRouter from "next/navigation" (line 31) should be grouped at the top, before lucide-react and component imports.

Suggested ordering
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+
 import {
   ChevronRight,
   CreditCard,
   LogOut,
   Receipt,
   MessageCircle,
 } from "lucide-react";
 
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 ...
 import { signOut } from "firebase/auth";
 import { auth } from "@/configs/Firebase-config";
-import { useRouter } from "next/navigation";
-import Link from "next/link";
 import posthog from "posthog-js";
 import { toast } from "sonner";
-import { useState } from "react";

As per coding guidelines: "Order imports: React/Next imports first, then components, then utilities."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/minors/nav-user.tsx` around lines 34 - 35, Reorder imports
in nav-user.tsx so React/Next imports appear first: move useState (from "react")
and useRouter (from "next/navigation") to the top of the import list, followed
by component imports (e.g., Avatar, AvatarImage, AvatarFallback, DropdownMenu,
DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem) and then utilities
like toast (from "sonner") and icons from "lucide-react"; ensure the import
grouping keeps the existing named symbols unchanged (useState, useRouter, toast,
Avatar, DropdownMenu, etc.) and preserves existing functionality.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/Layouts/minors/nav-user.tsx`:
- Around line 50-56: The handler handleOpenPlainChat currently silently no-ops
when window.Plain is undefined; update it to detect absence of the Plain SDK and
provide a fallback (e.g., show a toast/notification or open a support URL).
Specifically, in handleOpenPlainChat check if window.Plain exists and call
window.Plain.open(), otherwise call your app's toast/notification utility (or
window.open to a support URL) and log/report the missing SDK so users receive
immediate feedback. Ensure TypeScript ignores remain limited to the SDK access
and keep the original successful path unchanged.

---

Outside diff comments:
In `@components/Layouts/minors/nav-user.tsx`:
- Line 151: Replace the hardcoded AvatarFallback text "CN" with the same
user-initials logic used by the trigger avatar (e.g. use user.name?.charAt(0) ||
"U" or a shared getInitials(user.name) helper); update the AvatarFallback
element (symbol: AvatarFallback) inside the dropdown so it derives initials from
user.name rather than the literal "CN" to keep both avatars consistent.

---

Duplicate comments:
In `@components/Layouts/minors/nav-user.tsx`:
- Around line 44-111: The block defining the emailVerified prop,
isSendingVerification state and handleResendVerification is dead because
handleResendVerification is never used; either wire it into the UI (e.g., call
handleResendVerification from the appropriate menu item or button in
nav-user.tsx and show a sending state using isSendingVerification, and use the
emailVerified prop to conditionally render the verification menu entry), or
remove the unused items (remove emailVerified from the component props, delete
the isSendingVerification useState and the handleResendVerification function)
and also remove the now-unused imports CheckCircle2, AlertCircle, Mail, toast
and useState if they become unused elsewhere. Ensure references to
auth.currentUser/getIdToken and NEXT_PUBLIC_BASE_URL are removed if you delete
the function to avoid leftover unused variables.
- Around line 8-10: The imports CheckCircle2, AlertCircle, and Mail in the
NavUser component (nav-user.tsx) are unused; remove these three symbols from the
import list so they are no longer imported into the NavUser module to prevent
dead code and reduce bundle size. Locate the import statement that includes
CheckCircle2, AlertCircle, and Mail and delete those identifiers (leave any
other icons in that same import intact), then run the build/linters to confirm
no remaining references.

---

Nitpick comments:
In `@components/Layouts/minors/nav-user.tsx`:
- Around line 34-35: Reorder imports in nav-user.tsx so React/Next imports
appear first: move useState (from "react") and useRouter (from
"next/navigation") to the top of the import list, followed by component imports
(e.g., Avatar, AvatarImage, AvatarFallback, DropdownMenu, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuItem) and then utilities like toast (from
"sonner") and icons from "lucide-react"; ensure the import grouping keeps the
existing named symbols unchanged (useState, useRouter, toast, Avatar,
DropdownMenu, etc.) and preserves existing functionality.

Comment on lines +50 to +56
const handleOpenPlainChat = () => {
// @ts-ignore
if (window.Plain) {
// @ts-ignore
window.Plain.open?.();
}
};

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 | 🟡 Minor

Silent no-op when Plain SDK isn't loaded — user gets no feedback.

If window.Plain is undefined (SDK not yet loaded or failed to load), clicking "Chat with Support" does nothing with no indication to the user. Consider showing a fallback toast or opening a support URL.

Suggested improvement
  const handleOpenPlainChat = () => {
    // `@ts-ignore`
    if (window.Plain) {
      // `@ts-ignore`
      window.Plain.open?.();
+   } else {
+     toast.error("Support chat is temporarily unavailable. Please try again shortly.");
    }
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/minors/nav-user.tsx` around lines 50 - 56, The handler
handleOpenPlainChat currently silently no-ops when window.Plain is undefined;
update it to detect absence of the Plain SDK and provide a fallback (e.g., show
a toast/notification or open a support URL). Specifically, in
handleOpenPlainChat check if window.Plain exists and call window.Plain.open(),
otherwise call your app's toast/notification utility (or window.open to a
support URL) and log/report the missing SDK so users receive immediate feedback.
Ensure TypeScript ignores remain limited to the SDK access and keep the original
successful path unchanged.

@github-actions

Copy link
Copy Markdown

Cloud Run service deployed: https://pr-update-plain-support-cj6r7x3fpa-uc.a.run.app

@github-actions

Copy link
Copy Markdown

Cloud Run service deployed: https://pr-update-plain-support-cj6r7x3fpa-uc.a.run.app

@coderabbitai coderabbitai 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.

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)
components/Layouts/Sidebar.tsx (1)

231-249: ⚠️ Potential issue | 🟠 Major

Nested <button> inside <button> — invalid HTML and handleClick fires twice.

When link.showProModal || isWorkflowsLink is true:

  • Line 232: asChild={false}SidebarMenuButton renders as a <button> with onClick={handleClick} (line 235).
  • Lines 238–241: a second <button onClick={handleClick}> is rendered inside it.

This is invalid HTML (browsers promote or suppress nested interactive elements), and every click dispatches handleClick twice — once from the inner button and once as the event bubbles to the outer button. For the workflows path the isCheckingBackend guard does not protect against the double-call because both handlers fire synchronously in the same tick, before any React state update.

🐛 Proposed fix — use `asChild` consistently and let each child own its handler
  <SidebarMenuButton
-   asChild={!link.showProModal && !isWorkflowsLink}
+   asChild
    isActive={isActive}
    disabled={link.disabled || (isWorkflowsLink && isCheckingBackend)}
-   onClick={link.showProModal || isWorkflowsLink ? handleClick : link.handleTrack ? handleTrack : undefined}
  >
    {link.showProModal || isWorkflowsLink ? (
      <button
        className="flex gap-2 items-center w-full"
        onClick={handleClick}
      >
        ...
      </button>
    ) : (
      <Link
        href={link.href}
        className="flex gap-2 items-center w-full"
+       onClick={link.handleTrack ? handleTrack : undefined}
      >
        ...
      </Link>
    )}
  </SidebarMenuButton>

With asChild always enabled, SidebarMenuButton uses Radix Slot to forward its styling/active state to the single child element; each branch owns its own click handler and there is no nested interactive element.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/Sidebar.tsx` around lines 231 - 249, The current JSX nests
a button inside SidebarMenuButton when link.showProModal || isWorkflowsLink is
true, causing invalid HTML and double-firing handleClick; fix by always using
asChild={true} on <SidebarMenuButton> so it renders a Slot and let the single
child element own its onClick handler (remove the outer onClick on
SidebarMenuButton), and ensure branches for link.showProModal or isWorkflowsLink
attach handleClick only on the child button; keep existing disabled logic
(link.disabled || (isWorkflowsLink && isCheckingBackend)) on the child so the
guard still applies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/Layouts/minors/nav-user.tsx`:
- Around line 103-104: Remove the PII-leaking console.log that prints the user's
email in the success flow: locate the toast.success call in the handler (the
lines that use toast.success(`Verification email sent to ${data.email ||
user.email}...`)) and delete the console.log("Verification email sent
successfully to work email:", data.email || user.email); statement, or replace
it with a non-PII debug message (e.g., a generic "verification email sent"
message) if an internal log is needed.
- Around line 76-85: Remove the hardcoded 'http://localhost:8000' fallback in
the fetch call so a missing NEXT_PUBLIC_BASE_URL fails fast: update the fetch
invocation in components/Layouts/minors/nav-user.tsx to require
process.env.NEXT_PUBLIC_BASE_URL (no default), and add a quick runtime check
that throws or logs a clear error if NEXT_PUBLIC_BASE_URL is undefined before
calling fetch (reference the fetch call that posts to
/api/v1/account/resend-verification and the NEXT_PUBLIC_BASE_URL env var).

---

Outside diff comments:
In `@components/Layouts/Sidebar.tsx`:
- Around line 231-249: The current JSX nests a button inside SidebarMenuButton
when link.showProModal || isWorkflowsLink is true, causing invalid HTML and
double-firing handleClick; fix by always using asChild={true} on
<SidebarMenuButton> so it renders a Slot and let the single child element own
its onClick handler (remove the outer onClick on SidebarMenuButton), and ensure
branches for link.showProModal or isWorkflowsLink attach handleClick only on the
child button; keep existing disabled logic (link.disabled || (isWorkflowsLink &&
isCheckingBackend)) on the child so the guard still applies.

---

Duplicate comments:
In `@components/Layouts/minors/nav-user.tsx`:
- Around line 50-56: handleOpenPlainChat currently silently no-ops if
window.Plain is missing; update it to provide a user-facing fallback by
detecting absence of window.Plain (in handleOpenPlainChat) and then either
showing a toast/error notification or redirecting to a fallback support
URL/modal so the user gets feedback; keep the existing path that calls
window.Plain.open() when available, but add an explicit branch that triggers a
UI fallback (e.g., call showToast('Support is unavailable, please try again
later') or openSupportPage()) so clicks never do nothing.
- Around line 8-11: The file has unused icon imports (CheckCircle2, AlertCircle,
Mail) and dead/unwired logic (handleResendVerification function and
emailVerified prop). Fix by either: A) remove the unused imports from
nav-user.tsx and delete the handleResendVerification function and the
emailVerified prop from the component signature, or B) wire them properly by
passing emailVerified from Sidebar.tsx into the nav-user component and attach
handleResendVerification to the appropriate menu item (e.g., the verification
menu action) so the guard that checks emailVerified is reachable; update any JSX
to reference the used icons only (MessageCircle) and ensure no unused symbols
remain.

Comment on lines +76 to +85
const response = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:8000'}/api/v1/account/resend-verification`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);

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 | 🟠 Major

Hardcoded localhost:8000 fallback will silently fail in production.

If NEXT_PUBLIC_BASE_URL is unset in a deployed environment, the fetch targets http://localhost:8000, which causes a network error rather than surfacing the missing env-var as a configuration mistake. Remove the fallback so a missing env var fails fast and loudly (ideally validated at build time).

🐛 Proposed fix
- const response = await fetch(
-   `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:8000'}/api/v1/account/resend-verification`,
+ if (!process.env.NEXT_PUBLIC_BASE_URL) {
+   throw new Error("NEXT_PUBLIC_BASE_URL is not configured");
+ }
+ const response = await fetch(
+   `${process.env.NEXT_PUBLIC_BASE_URL}/api/v1/account/resend-verification`,
📝 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
const response = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:8000'}/api/v1/account/resend-verification`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
if (!process.env.NEXT_PUBLIC_BASE_URL) {
throw new Error("NEXT_PUBLIC_BASE_URL is not configured");
}
const response = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/v1/account/resend-verification`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/minors/nav-user.tsx` around lines 76 - 85, Remove the
hardcoded 'http://localhost:8000' fallback in the fetch call so a missing
NEXT_PUBLIC_BASE_URL fails fast: update the fetch invocation in
components/Layouts/minors/nav-user.tsx to require
process.env.NEXT_PUBLIC_BASE_URL (no default), and add a quick runtime check
that throws or logs a clear error if NEXT_PUBLIC_BASE_URL is undefined before
calling fetch (reference the fetch call that posts to
/api/v1/account/resend-verification and the NEXT_PUBLIC_BASE_URL env var).

Comment on lines +103 to +104
toast.success(`Verification email sent to ${data.email || user.email}! Please check your inbox (and spam folder).`);
console.log("Verification email sent successfully to work email:", data.email || user.email);

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 | 🟠 Major

console.log leaks PII (email address) to the browser console.

The toast.success call on line 103 already conveys success to the user. The console.log on line 104 is redundant and exposes the user's email address in plaintext in any browser devtools session, including those of browser extensions.

🐛 Proposed fix
  toast.success(`Verification email sent to ${data.email || user.email}! Please check your inbox (and spam folder).`);
- console.log("Verification email sent successfully to work email:", data.email || user.email);
📝 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
toast.success(`Verification email sent to ${data.email || user.email}! Please check your inbox (and spam folder).`);
console.log("Verification email sent successfully to work email:", data.email || user.email);
toast.success(`Verification email sent to ${data.email || user.email}! Please check your inbox (and spam folder).`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Layouts/minors/nav-user.tsx` around lines 103 - 104, Remove the
PII-leaking console.log that prints the user's email in the success flow: locate
the toast.success call in the handler (the lines that use
toast.success(`Verification email sent to ${data.email || user.email}...`)) and
delete the console.log("Verification email sent successfully to work email:",
data.email || user.email); statement, or replace it with a non-PII debug message
(e.g., a generic "verification email sent" message) if an internal log is
needed.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
app/plain-chat.tsx (4)

49-83: IIFE wrapper is unnecessary; document is already in scope.

The (function(d) { ... })(document) pattern is a copy-paste artifact from Plain's standalone embed snippet. Inside a React module, document is already directly accessible.

♻️ Proposed simplification
-        (function (d) {
-            const script = d.createElement("script");
+        const script = document.createElement("script");
             script.id = "plain-chat-script";
             script.async = false;
             script.onload = function () { ... };
             script.src = "https://chat.cdn-plain.com/index.js";
-            d.getElementsByTagName("head")[0].appendChild(script);
-        })(document);
+        document.head.appendChild(script);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` around lines 49 - 83, The IIFE wrapper around the embed
is unnecessary in this React module; remove the self-invoking function (the
(function(d){ ... })(document) wrapper) and its parameter 'd', and use document
directly when creating and appending the script element in app/plain-chat.tsx;
keep the existing script id "plain-chat-script", script.async, script.onload
handler (including the window.Plain.init call and its config), script.src, and
the appendChild to document.getElementsByTagName("head")[0] unchanged.

54-57: Replace @ts-ignore with a proper Window interface augmentation.

Using @ts-ignore silently bypasses type checking on the window.Plain.init(...) call. Any typo in appId, hideLauncher, or the links shape will go undetected. Declare the shape in a *.d.ts file or inline:

♻️ Proposed type augmentation
+"use client";
+
+interface PlainLinkItem {
+  icon: string;
+  text: string;
+  url: string;
+}
+
+interface PlainInitOptions {
+  appId: string;
+  hideLauncher?: boolean;
+  theme?: "light" | "dark";
+  links?: PlainLinkItem[];
+}
+
+declare global {
+  interface Window {
+    Plain?: {
+      init: (options: PlainInitOptions) => void;
+    };
+  }
+}

Then in the onload handler:

-                // `@ts-ignore`
                 if (window.Plain) {
-                // `@ts-ignore`
                     window.Plain.init({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` around lines 54 - 57, Replace the two uses of
"@ts-ignore" around window.Plain.init by adding a proper Window augmentation
that declares the Plain object and its init(options) signature: create a .d.ts
(or inline declaration) that defines interface PlainInitOptions { appId: string;
hideLauncher?: boolean; links?: Array<{ label: string; url: string }>; /* add
other known fields */ } and interface PlainWindow { Plain?: { init: (opts:
PlainInitOptions) => void } } and merge it into global Window; then remove the
`@ts-ignore` lines in app/plain-chat.tsx so TypeScript validates the call to
window.Plain.init and the shapes of appId, hideLauncher, and links.

5-5: Add FC type annotation to the component.

The component is currently untyped. As per coding guidelines, functional components should use TypeScript FC types.

-export default function PlainChatProvider() {
+import type { FC } from "react";
+
+const PlainChatProvider: FC = () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` at line 5, The PlainChatProvider component is untyped;
import React and the FC type from 'react' and change the declaration from a
plain function to a typed functional component (e.g., declare PlainChatProvider
as a const with the FC type: PlainChatProvider: FC = () => { ... }) and then
export it as the default export (export default PlainChatProvider) so the
component uses the TypeScript FC annotation; reference the PlainChatProvider
symbol when making these changes.

58-58: Consider externalizing appId to an environment variable.

The app ID is currently hardcoded. While it's not a secret, using process.env.NEXT_PUBLIC_PLAIN_APP_ID makes it easy to use a different widget instance across environments (staging, production).

♻️ Proposed change
-                        appId: "liveChatApp_01KHRDQSB5WJHKCW4ZE3GFH7K5",
+                        appId: process.env.NEXT_PUBLIC_PLAIN_APP_ID ?? "liveChatApp_01KHRDQSB5WJHKCW4ZE3GFH7K5",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` at line 58, Replace the hardcoded appId with an
environment-backed value: read process.env.NEXT_PUBLIC_PLAIN_APP_ID where the
appId property is set (the appId symbol in the Plain chat widget config) and use
a sensible fallback or runtime warning if the env var is missing; also ensure
NEXT_PUBLIC_PLAIN_APP_ID is defined in your Next/hosting env so the widget can
target different instances per environment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/plain-chat.tsx`:
- Around line 53-80: The script insertion currently only sets script.onload and
has no error handling, so if loading "https://chat.cdn-plain.com/index.js" fails
the chat silently disappears; add a script.onerror handler alongside
script.onload that logs the failure (include the URL and the error/event) using
the app logger or console.error and optionally sets a fallback state/flag so the
UI can show a degraded message; locate the script creation block that assigns
script.onload and the window.Plain.init call and add the onerror handler there
to surface network/CSP/load errors and avoid silent failures.
- Around line 50-82: The injected Plain chat script (created as script with id
"plain-chat-script" and referenced via window.Plain.init) lacks crossOrigin and
no Content Security Policy is declared; add crossOrigin="anonymous" when
creating the script element to enable CORS-safe error reporting, and ensure your
app serves a strict CSP that allowlists the Plain CDN (e.g., include
https://chat.cdn-plain.com in script-src) so third‑party scripts are
restricted—update the code that creates the script element and your server/meta
headers to apply these changes.

---

Nitpick comments:
In `@app/plain-chat.tsx`:
- Around line 49-83: The IIFE wrapper around the embed is unnecessary in this
React module; remove the self-invoking function (the (function(d){ ...
})(document) wrapper) and its parameter 'd', and use document directly when
creating and appending the script element in app/plain-chat.tsx; keep the
existing script id "plain-chat-script", script.async, script.onload handler
(including the window.Plain.init call and its config), script.src, and the
appendChild to document.getElementsByTagName("head")[0] unchanged.
- Around line 54-57: Replace the two uses of "@ts-ignore" around
window.Plain.init by adding a proper Window augmentation that declares the Plain
object and its init(options) signature: create a .d.ts (or inline declaration)
that defines interface PlainInitOptions { appId: string; hideLauncher?: boolean;
links?: Array<{ label: string; url: string }>; /* add other known fields */ }
and interface PlainWindow { Plain?: { init: (opts: PlainInitOptions) => void } }
and merge it into global Window; then remove the `@ts-ignore` lines in
app/plain-chat.tsx so TypeScript validates the call to window.Plain.init and the
shapes of appId, hideLauncher, and links.
- Line 5: The PlainChatProvider component is untyped; import React and the FC
type from 'react' and change the declaration from a plain function to a typed
functional component (e.g., declare PlainChatProvider as a const with the FC
type: PlainChatProvider: FC = () => { ... }) and then export it as the default
export (export default PlainChatProvider) so the component uses the TypeScript
FC annotation; reference the PlainChatProvider symbol when making these changes.
- Line 58: Replace the hardcoded appId with an environment-backed value: read
process.env.NEXT_PUBLIC_PLAIN_APP_ID where the appId property is set (the appId
symbol in the Plain chat widget config) and use a sensible fallback or runtime
warning if the env var is missing; also ensure NEXT_PUBLIC_PLAIN_APP_ID is
defined in your Next/hosting env so the widget can target different instances
per environment.

Comment thread app/plain-chat.tsx
Comment on lines +50 to +82
const script = d.createElement("script");
script.id = "plain-chat-script";
script.async = false;
script.onload = function () {
// @ts-ignore
if (window.Plain) {
// @ts-ignore
window.Plain.init({
appId: "liveChatApp_01KHRDQSB5WJHKCW4ZE3GFH7K5",
hideLauncher: true,
theme: 'light',
links: [
{
icon: 'book',
text: 'Documentation',
url: 'https://docs.potpie.ai/introduction',
},
{
icon: 'link',
text: 'Open Source',
url: 'https://github.com/potpie-ai/potpie',
},
{
icon: 'discord',
text: 'Join our Discord',
url: 'https://discord.gg/ryk5CMD5v6',
},
],
});
}
};
script.src = "https://chat.cdn-plain.com/index.js";
d.getElementsByTagName("head")[0].appendChild(script);

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

curl -sI "https://chat.cdn-plain.com/index.js" | grep -iE 'cache-control|etag|last-modified|content-type|x-version'

Repository: potpie-ai/potpie-ui

Length of output: 259


Add crossOrigin="anonymous" and implement Content Security Policy — third-party script lacks integrity verification.

The Plain CDN serves a dynamically updated script (4-hour cache, ETags indicate frequent updates), making Subresource Integrity (SRI) impractical without continuous CI/CD hash updates. However, crossOrigin="anonymous" should still be added for consistency and CORS-safe error reporting.

Primary mitigation: Implement a strict Content Security Policy that explicitly allowlists https://chat.cdn-plain.com in the script-src directive. This prevents injection of unauthorized scripts from compromised or misconfigured CDNs.

Without CSP or SRI, a compromised chat.cdn-plain.com would have full DOM access and could exfiltrate user data, modify page content, or inject malicious payloads into every session.

📋 Recommended changes
 const script = d.createElement("script");
 script.id = "plain-chat-script";
 script.async = false;
+script.crossOrigin = "anonymous";
 script.onload = function () {

Add to CSP header or <meta> tag:

Content-Security-Policy: script-src 'self' https://chat.cdn-plain.com;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` around lines 50 - 82, The injected Plain chat script
(created as script with id "plain-chat-script" and referenced via
window.Plain.init) lacks crossOrigin and no Content Security Policy is declared;
add crossOrigin="anonymous" when creating the script element to enable CORS-safe
error reporting, and ensure your app serves a strict CSP that allowlists the
Plain CDN (e.g., include https://chat.cdn-plain.com in script-src) so
third‑party scripts are restricted—update the code that creates the script
element and your server/meta headers to apply these changes.

Comment thread app/plain-chat.tsx
Comment on lines +53 to +80
script.onload = function () {
// @ts-ignore
if (window.Plain) {
// @ts-ignore
window.Plain.init({
appId: "liveChatApp_01KHRDQSB5WJHKCW4ZE3GFH7K5",
hideLauncher: true,
theme: 'light',
links: [
{
icon: 'book',
text: 'Documentation',
url: 'https://docs.potpie.ai/introduction',
},
{
icon: 'link',
text: 'Open Source',
url: 'https://github.com/potpie-ai/potpie',
},
{
icon: 'discord',
text: 'Join our Discord',
url: 'https://discord.gg/ryk5CMD5v6',
},
],
});
}
};

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 | 🟡 Minor

No onerror handler — script load failure is silent.

If https://chat.cdn-plain.com/index.js fails to load (CDN outage, network error, CSP block), onload never fires and the chat widget is silently unavailable with no logging.

🛡️ Proposed fix
 script.onload = function () {
     if (window.Plain) {
         window.Plain.init({ ... });
     }
 };
+script.onerror = function () {
+    console.error("[PlainChat] Failed to load Plain chat script from CDN.");
+};
📝 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
script.onload = function () {
// @ts-ignore
if (window.Plain) {
// @ts-ignore
window.Plain.init({
appId: "liveChatApp_01KHRDQSB5WJHKCW4ZE3GFH7K5",
hideLauncher: true,
theme: 'light',
links: [
{
icon: 'book',
text: 'Documentation',
url: 'https://docs.potpie.ai/introduction',
},
{
icon: 'link',
text: 'Open Source',
url: 'https://github.com/potpie-ai/potpie',
},
{
icon: 'discord',
text: 'Join our Discord',
url: 'https://discord.gg/ryk5CMD5v6',
},
],
});
}
};
script.onload = function () {
// `@ts-ignore`
if (window.Plain) {
// `@ts-ignore`
window.Plain.init({
appId: "liveChatApp_01KHRDQSB5WJHKCW4ZE3GFH7K5",
hideLauncher: true,
theme: 'light',
links: [
{
icon: 'book',
text: 'Documentation',
url: 'https://docs.potpie.ai/introduction',
},
{
icon: 'link',
text: 'Open Source',
url: 'https://github.com/potpie-ai/potpie',
},
{
icon: 'discord',
text: 'Join our Discord',
url: 'https://discord.gg/ryk5CMD5v6',
},
],
});
}
};
script.onerror = function () {
console.error("[PlainChat] Failed to load Plain chat script from CDN.");
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/plain-chat.tsx` around lines 53 - 80, The script insertion currently only
sets script.onload and has no error handling, so if loading
"https://chat.cdn-plain.com/index.js" fails the chat silently disappears; add a
script.onerror handler alongside script.onload that logs the failure (include
the URL and the error/event) using the app logger or console.error and
optionally sets a fallback state/flag so the UI can show a degraded message;
locate the script creation block that assigns script.onload and the
window.Plain.init call and add the onerror handler there to surface
network/CSP/load errors and avoid silent failures.

@github-actions

Copy link
Copy Markdown

Cloud Run service deployed: https://pr-update-plain-support-cj6r7x3fpa-uc.a.run.app

Sign up for free to 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