Skip to content

agent: @U0AJM7X8FBR we started this PR, it I want to make it better. Current: #1580

Open
sweetmantech wants to merge 10 commits intotestfrom
agent/-u0ajm7x8fbr-we-started-this-p-1773492278196
Open

agent: @U0AJM7X8FBR we started this PR, it I want to make it better. Current: #1580
sweetmantech wants to merge 10 commits intotestfrom
agent/-u0ajm7x8fbr-we-started-this-p-1773492278196

Conversation

@sweetmantech
Copy link
Copy Markdown
Collaborator

@sweetmantech sweetmantech commented Mar 14, 2026

Automated PR from coding agent.

Summary by CodeRabbit

  • New Features

    • Multi-step onboarding wizard (welcome, role, context, artists, connections, pulse, tasks, complete) with progress UI, animated completion, confetti, and navigation controls.
    • Integrated onboarding modal into the Home screen.
    • Spotify artist search, priority-artist management, connector connection flow, and Pulse toggle included in onboarding.
  • Chores

    • Onboarding progress and settings now persist to the user account and trigger related post-onboard actions.

Recoup Agent and others added 8 commits March 12, 2026 03:41
…etion

The /api/account/update route was ignoring onboardingStatus and
onboardingData from the request body, so onboarding_status was never
persisted. On page reload the modal re-opened because the DB still had
null. Also moved persist() to run in handleComplete (awaited before
redirect) instead of on the tasks step, ensuring fresh data and
guaranteed completion before the page navigates away.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 14, 2026

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

Project Deployment Actions Updated (UTC)
recoup-chat Ready Ready Preview Mar 14, 2026 10:52pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 14, 2026

📝 Walkthrough

Walkthrough

Adds a multi-step onboarding wizard (welcome → complete) with new UI steps, Spotify artist search, connector OAuth flows, confetti animation, persistence hook that saves artists/updates pulse and posts onboarding data to the account API, and mounts the modal on the HomePage.

Changes

Cohort / File(s) Summary
API Endpoint Update
app/api/account/update/route.tsx
POST handler now accepts and conditionally includes onboardingStatus and onboardingData when creating or updating account_info.
HomePage Integration
components/Home/HomePage.tsx
Imports and renders OnboardingModal above the existing <Chat />.
Onboarding Orchestrator
components/Onboarding/OnboardingModal.tsx
New modal orchestrator that composes steps, manages navigation/state via useOnboarding, persists data on completion, and may redirect when artists exist.
Wizard Steps (UI)
components/Onboarding/OnboardingWelcomeStep.tsx, .../OnboardingRoleStep.tsx, .../OnboardingContextStep.tsx, .../OnboardingArtistsStep.tsx, .../OnboardingConnectionsStep.tsx, .../OnboardingPulseStep.tsx, .../OnboardingTasksStep.tsx, .../OnboardingCompleteStep.tsx
Eight new step components implementing welcome, role selection, context inputs, Spotify artist search & manual add, connector OAuth connect flow, pulse toggle, role-based task queue, and completion UI with animations/confetti.
Navigation & Progress UI
components/Onboarding/OnboardingNavButtons.tsx, components/Onboarding/OnboardingStepDots.tsx
Reusable back/next navigation buttons and a step-dot progress indicator for the wizard.
Visual/Animation
components/Onboarding/OnboardingConfetti.tsx
New Framer Motion-based confetti component that renders ~60 animated particles and auto-hides.
Role Config
components/Onboarding/onboardingRoleConfig.ts
Centralized role metadata, ROLE_CONFIG_MAP, and getRoleConfig helper for role-specific labels/placeholders.
Onboarding State Hook
components/Onboarding/useOnboarding.ts
Hook managing isOpen, current step, and incremental onboarding data with next/prev/complete controls and auto-open based on user data.
Persistence Hook
components/Onboarding/useOnboardingPersist.ts
Persist orchestration: creates artists (parallel), enables Pulse (if requested), POSTs onboarding payload to /api/account/update, and refreshes artist list.
Spotify Search Hook
components/Onboarding/useSpotifyArtistSearch.ts
Debounced (320ms) Spotify artist search hook exposing query, results, searching flag, and a clearResults helper.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Home as HomePage
    participant Modal as OnboardingModal
    participant Steps as Step Components
    participant Search as useSpotifyArtistSearch
    participant OAuth as Connector OAuth
    participant Persist as useOnboardingPersist
    participant API as Backend/API

    User->>Home: Open app
    Home->>Modal: Render OnboardingModal
    User->>Modal: Interact / Next
    Modal->>Steps: Render current step
    Steps->>Search: Query artist (debounced)
    Search-->>Steps: Artist results
    User->>Steps: Add artist / trigger connect
    Steps->>OAuth: Open connector popup / authorize
    OAuth-->>Steps: Connection completed (popup closed)
    Modal->>Persist: persist(onboardingData)
    Persist->>API: POST /api/account/update (onboardingData)
    Persist->>API: saveArtist (parallel calls)
    Persist->>API: updatePulse (if enabled)
    Persist-->>Modal: done
    Modal-->>User: Close modal / optional redirect
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

✨ A modal blooms, step-by-step we glide,
Search finds the artists, connectors tied.
Confetti dances, a pulse may hum,
Data saved, the final step — we're done.
🚀 Welcome aboard, the journey's begun!

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning PR violates SRP with useOnboardingPersist managing 5+ concerns, DRY with 8 duplicate step components, and contains state management bugs noted in review comments. Extract persistence by domain, create abstract StepLayout component, implement durable completion latch keyed to account ID, track multiple OAuth flows with Set, add error notifications.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch agent/-u0ajm7x8fbr-we-started-this-p-1773492278196
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 508792c2c5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

callbackUrl: window.location.href,
});
if (url) {
onConnect(slug);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Confirm OAuth success before marking connector connected

This marks a connector as connected as soon as an authorization URL is returned, but before the user completes OAuth in the new tab. If the user closes the flow or denies access, onboarding still counts the connector as connected and persists an inflated connectedCount, which produces incorrect onboarding data and misleading UI state.

Useful? React with 👍 / 👎.

<OnboardingConnectionsStep
connected={data.connectedSlugs ?? []}
onConnect={slug =>
updateData({ connectedSlugs: [...(data.connectedSlugs ?? []), slug] })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Append connected slugs from latest state

This append is computed from render-time data.connectedSlugs, so concurrent connector flows can overwrite each other: if two onConnect callbacks resolve from the same render, each uses the same stale base array and the later write drops the earlier slug. This loses connection selections when users trigger multiple connects quickly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (11)
components/Onboarding/OnboardingNavButtons.tsx (1)

3-8: Consider exporting the props interface.

Per coding guidelines, component prop types should be exported with explicit ComponentNameProps naming convention for better reusability and documentation.

♻️ Suggested change
-interface OnboardingNavButtonsProps {
+export interface OnboardingNavButtonsProps {
   onBack: () => void;
   onNext: () => void;
   nextLabel?: string;
   nextDisabled?: boolean;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingNavButtons.tsx` around lines 3 - 8, The props
interface OnboardingNavButtonsProps should be exported so other modules can
import and reuse it; update the declaration of the interface
OnboardingNavButtonsProps to be exported (e.g., add the export keyword before
the interface) and ensure any local usages/imports remain valid after making the
interface exportable.
components/Onboarding/onboardingRoleConfig.ts (1)

70-80: Consider referencing the existing "other" role to avoid duplication.

The fallback object in getRoleConfig duplicates the "other" role definition. Reference the existing config to stay DRY.

♻️ Suggested refactor
 export function getRoleConfig(roleId: string | undefined): RoleConfig {
-  return (
-    ROLE_CONFIG_MAP[roleId ?? ""] ?? {
-      id: "other",
-      label: "Other",
-      icon: "✨",
-      description: "",
-      companyLabel: "Company",
-      artistPlaceholder: "Search for an artist…",
-    }
-  );
+  return ROLE_CONFIG_MAP[roleId ?? ""] ?? ROLE_CONFIG_MAP["other"];
 }

Note: This assumes "other" will always exist in ONBOARDING_ROLES. If that's not guaranteed, add a runtime check or keep the inline fallback.

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

In `@components/Onboarding/onboardingRoleConfig.ts` around lines 70 - 80,
getRoleConfig currently returns an inline "other" object as a fallback which
duplicates the existing role; change it to reference the canonical entry in
ROLE_CONFIG_MAP (e.g., use ROLE_CONFIG_MAP["other"]) as the default, and if
"other" may not always exist add a runtime guard that falls back to a minimal
inline object only when ROLE_CONFIG_MAP["other"] is undefined; update
getRoleConfig to use ROLE_CONFIG_MAP[roleId ?? ""] ?? ROLE_CONFIG_MAP["other"]
(with the extra guard if needed).
components/Onboarding/OnboardingStepDots.tsx (2)

4-11: Unused label field in STEPS array.

Each step includes a label property that's never rendered. Either remove the unused field to keep the code lean, or render the labels to provide better user context.

♻️ Option A: Remove unused labels
-const STEPS: { id: OnboardingStep; label: string }[] = [
-  { id: "role", label: "Role" },
-  { id: "context", label: "You" },
-  { id: "artists", label: "Artists" },
-  { id: "connections", label: "Connect" },
-  { id: "pulse", label: "Pulse" },
-  { id: "tasks", label: "Tasks" },
-];
+const STEPS: OnboardingStep[] = [
+  "role", "context", "artists", "connections", "pulse", "tasks"
+];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingStepDots.tsx` around lines 4 - 11, The STEPS
constant contains an unused label field (const STEPS: { id: OnboardingStep;
label: string }[]) in OnboardingStepDots.tsx; either remove the label property
from the STEPS entries and their type to clean up dead data, or update the
OnboardingStepDots component to render the label (e.g., next to each dot) so the
label is used—locate the STEPS array and the rendering logic in
OnboardingStepDots (and the OnboardingStep type) and apply one of these two
changes consistently.

23-56: Consider adding ARIA attributes for accessibility.

The step progress indicator lacks screen reader context. Adding semantic attributes would improve accessibility for users relying on assistive technologies.

♿ Suggested accessibility improvements
-    <div className="flex items-center justify-center gap-0 w-full">
+    <div 
+      className="flex items-center justify-center gap-0 w-full"
+      role="group"
+      aria-label={`Onboarding progress: step ${currentIdx + 1} of ${STEPS.length}`}
+    >
       {STEPS.map((step, i) => {
         const isCompleted = i < currentIdx;
         const isCurrent = i === currentIdx;

         return (
-          <div key={step.id} className="flex items-center">
+          <div 
+            key={step.id} 
+            className="flex items-center"
+            aria-current={isCurrent ? "step" : undefined}
+          >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingStepDots.tsx` around lines 23 - 56, Update
the OnboardingStepDots component to expose semantic ARIA attributes: add
role="list" and an appropriate aria-label (e.g., "Onboarding progress") to the
outer container, set each step wrapper (the div keyed by step.id) to
role="listitem", and on the dot element include aria-current="step" when
isCurrent and a clear accessible name (e.g., the step.title/step.label via a
visually-hidden span or aria-label) so screen readers know which step is current
and what each dot represents; also mark future steps as aria-disabled="true" or
omit aria-current for non-current steps. Ensure you update the JSX near
STEPS.map, currentIdx, and the dot/container divs, using cn only for visual
classes.
app/api/account/update/route.tsx (1)

9-9: Consider adding type safety for the request body.

The destructured fields from body are implicitly typed as any. While this works, adding a TypeScript interface for the expected request payload would improve type safety and make the API contract explicit.

🔧 Suggested type definition
interface UpdateAccountRequest {
  accountId: string;
  instruction?: string;
  name?: string;
  organization?: string;
  image?: string;
  jobTitle?: string;
  roleType?: string;
  companyName?: string;
  knowledges?: unknown;
  onboardingStatus?: Record<string, unknown>;
  onboardingData?: Record<string, unknown>;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/account/update/route.tsx` at line 9, Create a TypeScript interface
(e.g. UpdateAccountRequest) that declares accountId: string and the optional
fields instruction, name, organization, image, jobTitle, roleType, companyName,
knowledges, onboardingStatus, and onboardingData with appropriate types (use
unknown or Record<string, unknown> where structure is not fixed), then annotate
the request body with that interface where you parse/destructure body (the
variable named body in the route handler) so the destructured line "const {
instruction, name, organization, accountId, image, jobTitle, roleType,
companyName, knowledges, onboardingStatus, onboardingData } = body;" is
type-checked against UpdateAccountRequest; adjust any downstream usages to
satisfy the new types.
components/Onboarding/OnboardingArtistsStep.tsx (1)

117-138: Consider using a stable key instead of array index for the artist list.

Using key={i} (line 120) can cause rendering issues if artists are reordered or removed from the middle. Since artists have unique name (with case-insensitive dedup) or spotifyUrl, consider using a composite key.

♻️ Use stable key for artist items
 {artists.map((a, i) => (
-  <li key={i} className="flex items-center gap-3 rounded-xl border bg-muted/20 px-3 py-2.5">
+  <li key={a.spotifyUrl ?? a.name.toLowerCase()} className="flex items-center gap-3 rounded-xl border bg-muted/20 px-3 py-2.5">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingArtistsStep.tsx` around lines 117 - 138, The
artist list uses the array index as the React key (key={i}) which is unstable;
update the list rendering in the component that maps artists (the artists.map in
OnboardingArtistsStep) to use a stable composite key such as `${a.spotifyUrl ??
a.name.toLowerCase()}` or similar so keys remain stable across
reorders/removals; keep the remove(i) call the same but ensure each <li> uses
that stable identifier instead of i and reference ArtistAvatar and the a object
fields (name, spotifyUrl, imageUrl) when constructing the key.
components/Onboarding/OnboardingConnectionsStep.tsx (1)

104-119: Good loading and disabled states on the Connect button.

The button properly shows a spinner during connection and is disabled to prevent double-clicks. Consider adding aria-label to indicate the external link behavior for screen readers.

♿ Accessibility enhancement
 <Button
   size="sm"
   variant="outline"
   onClick={() => handleConnect(c.slug)}
   disabled={isConnecting}
   className="shrink-0"
+  aria-label={`Connect ${c.name} (opens in new tab)`}
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingConnectionsStep.tsx` around lines 104 - 119,
The Connect button (Button) should include an accessible label indicating it
opens an external destination for screen readers; update the JSX where onClick
calls handleConnect(c.slug) and isConnecting controls state to pass an
aria-label like `Connect to ${c.slug} (opens external link)` or similar
descriptive text to the Button (or the ExternalLink icon if you prefer the label
on the icon), ensuring the label dynamically uses c.slug and preserving existing
isConnecting and disabled behavior.
components/Onboarding/OnboardingWelcomeStep.tsx (1)

119-129: Consider adding accessible labels for status indicators.

The emoji indicators (✅/⏳) are decorative but convey status information. Screen readers may not announce these meaningfully. Consider adding aria-label or aria-hidden with a visually hidden status text.

♿ Proposed accessibility improvement
 <motion.div
   key={i}
   initial={{ opacity: 0, x: -8 }}
   animate={{ opacity: 1, x: 0 }}
   className="flex items-center gap-2 text-sm"
 >
-  <span className="text-base">
+  <span className="text-base" aria-hidden="true">
     {i < lineIdx ? "✅" : "⏳"}
   </span>
-  <span className={i < lineIdx ? "text-foreground" : "text-foreground animate-pulse"}>
+  <span 
+    className={i < lineIdx ? "text-foreground" : "text-foreground animate-pulse"}
+    aria-label={i < lineIdx ? "Completed:" : "In progress:"}
+  >
     {line}
   </span>
 </motion.div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingWelcomeStep.tsx` around lines 119 - 129, The
emoji status indicators inside the OnboardingWelcomeStep (the motion.div that
renders {i < lineIdx ? "✅" : "⏳"} alongside {line}) are decorative but convey
state; update that rendering so the emoji itself is aria-hidden and add an
accessible status label for screen readers—either by adding an aria-label on the
motion.div (e.g., "completed" vs "in progress" based on i and lineIdx) or by
inserting a visually-hidden <span> with that status text next to the emoji;
ensure the logic uses the same i and lineIdx conditions so screen readers
receive the current status for each line.
components/Onboarding/OnboardingContextStep.tsx (1)

35-47: Consider the stale closure risk in useEffect dependencies.

The effect intentionally skips name and onChangeName to avoid re-running after manual edits, but this pattern can cause subtle bugs if the parent re-renders with different callback references. A safer approach is to use a ref to track whether pre-fill has occurred.

♻️ Alternative using a ref to track pre-fill state
+import { useEffect, useRef } from "react";
 
 export function OnboardingContextStep({
   ...
 }: Props) {
   const { userData, email } = useUserProvider();
+  const hasPrefilled = useRef(false);
 
   useEffect(() => {
-    if (!name && (userData?.name || email)) {
+    if (!hasPrefilled.current && !name && (userData?.name || email)) {
+      hasPrefilled.current = true;
       const inferred =
         userData?.name ||
         (email
           ? email
               .split("@")[0]
               .replace(/[._]/g, " ")
               .replace(/\b\w/g, c => c.toUpperCase())
           : "");
       if (inferred) onChangeName(inferred);
     }
-  }, [userData?.name, email]); // eslint-disable-line react-hooks/exhaustive-deps
+  }, [userData?.name, email, name, onChangeName]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingContextStep.tsx` around lines 35 - 47,
Replace the current useEffect with a version that uses a ref (e.g.,
hasPrefilledRef) to ensure pre-fill runs only once and avoid the stale-closure
risk: create const hasPrefilledRef = useRef(false), then in the effect check if
(!hasPrefilledRef.current && !name && (userData?.name || email)) { /* compute
inferred same as before */ if (inferred) { onChangeName(inferred);
hasPrefilledRef.current = true } }, and include onChangeName, userData?.name,
email (and optionally name) in the dependency array and remove the
eslint-disable comment so the effect sees updated callback references.
components/Onboarding/OnboardingCompleteStep.tsx (2)

97-98: Hide decorative emoji from assistive technologies.

Line 97 is decorative and duplicates Line 98 semantics; mark it aria-hidden for cleaner screen-reader output.

Suggested fix
-                <span className="text-lg">{item.icon}</span>
+                <span className="text-lg" aria-hidden="true">{item.icon}</span>
                 <span className="font-medium">{item.text}</span>

As per coding guidelines, **/*.{tsx,ts,jsx,js}: "Provide proper ARIA roles/states and test with screen readers".

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

In `@components/Onboarding/OnboardingCompleteStep.tsx` around lines 97 - 98, The
decorative emoji span in the OnboardingCompleteStep component (the <span>
rendering item.icon alongside item.text) duplicates the textual semantics and
should be hidden from assistive tech; update the icon span that renders
item.icon to include aria-hidden="true" (or aria-hidden={true}) so screen
readers ignore it while leaving the text span (item.text) unchanged.

8-25: Use an explicit exported props type name for the component API.

Replace Props with OnboardingCompleteStepProps and export it for clearer, consistent typing.

Suggested fix
-interface Props {
+export interface OnboardingCompleteStepProps {
   artistNames: string[];
   name: string | undefined;
   connectedCount: number;
   pulseEnabled: boolean;
   onComplete: () => void;
 }
@@
 export function OnboardingCompleteStep({
   artistNames,
   name,
   connectedCount,
   pulseEnabled,
   onComplete,
-}: Props) {
+}: OnboardingCompleteStepProps) {

As per coding guidelines, **/*.{tsx,ts}: "Export component prop types with explicit ComponentNameProps naming convention".

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

In `@components/Onboarding/OnboardingCompleteStep.tsx` around lines 8 - 25, Rename
the inline Props type to an exported, explicitly named type
OnboardingCompleteStepProps and update the component signature to use that type:
export type OnboardingCompleteStepProps = { artistNames: string[]; name: string
| undefined; connectedCount: number; pulseEnabled: boolean; onComplete: () =>
void; } and change the function parameter typing in OnboardingCompleteStep to
accept OnboardingCompleteStepProps instead of Props; ensure the new type is
exported so other modules can import it.
🤖 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/Onboarding/OnboardingCompleteStep.tsx`:
- Around line 13-14: The OnboardingCompleteStep component allows repeated clicks
to trigger duplicate completion actions; add a local boolean state (e.g.,
isCompleting) and use it as a guard in the completion handler and to disable the
completion button: in the handler (the function that currently calls
props.onComplete or performs persistence/redirect) return early if isCompleting
is true, set isCompleting = true at the start of work, await the async work,
then set isCompleting = false (or keep true if completed and component
unmounts); also set the button's disabled/aria-disabled prop based on
isCompleting to prevent rapid re-clicks.

In `@components/Onboarding/OnboardingConfetti.tsx`:
- Around line 54-62: The borderRadius is computed with Math.random() during
render which causes particle shapes to flip on re-renders; move the randomness
into the particle creation by adding a shape/borderRadius property on each
particle inside makeParticles() (e.g., p.borderRadius or p.isRound) and set it
once when particles are created, then replace the inline Math.random() in the
render style with that particle property (refer to makeParticles() and the
particle render that currently uses borderRadius: Math.random() > 0.5 ? "50%" :
"2px").

In `@components/Onboarding/OnboardingConnectionsStep.tsx`:
- Around line 52-69: The current handleConnect function calls onConnect(slug)
optimistically before OAuth completes, making the connector appear connected
even if the user cancels the flow; update handleConnect to NOT call onConnect
immediately — instead set a "pending" state for the slug (use setConnecting or a
new setPendingConnector) and open the OAuth URL via authorizeConnectorApi and
window.open; then wire the real completion path (the OAuth callback handling
code that receives the connector callback) to call onConnect(slug) only after
verifying the connector is actually authorized, or alternatively update the UI
to show "Pending…" or "Authorization opened in new tab" while waiting for the
callback rather than marking it connected in handleConnect.

In `@components/Onboarding/OnboardingModal.tsx`:
- Around line 96-98: The onConnect handler in OnboardingModal.tsx blindly
appends slug to data.connectedSlugs which can create duplicates and inflate
connectedCount; update the onConnect logic that calls updateData so it first
checks data.connectedSlugs (or uses a Set) and only adds the slug if it's not
already present (e.g., construct a new array from existing data.connectedSlugs
without duplicates, then call updateData with connectedSlugs set to that
deduplicated array).
- Around line 27-37: The handleComplete callback currently awaits persist(data)
but always calls complete() and triggers the redirect even if persist rejects;
wrap the await persist(...) in a try/catch inside handleComplete, call
complete() and run the setTimeout redirect only on successful persist, and in
the catch branch handle or surface the error (e.g., log or show validation) so
failures don't advance onboarding; reference the handleComplete function,
persist, complete, and the setTimeout redirect logic when making the change.

In `@components/Onboarding/OnboardingWelcomeStep.tsx`:
- Around line 43-57: The useEffect in OnboardingWelcomeStep sets a
setTimeout(onDone, 700) but never tracks or clears that timeout on unmount;
update the effect to store the timeout ID (e.g. const doneTimer =
setTimeout(...)) when scheduling onDone and clear it in the cleanup function
(clearTimeout(doneTimer)), and also clear any pending doneTimer right before
calling setTimeout inside the interval branch to avoid duplicates; reference the
useEffect, setInterval, setTimeout(onDone, 700), setLineIdx, setDone and onDone
symbols when making this change.

In `@components/Onboarding/useOnboardingPersist.ts`:
- Around line 43-63: The fetch call in useOnboardingPersist (inside the
persist/update block that POSTs to "/api/account/update") only catches network
errors and ignores non-OK HTTP responses; change it to await the fetch, check
response.ok, and handle non-OK by logging or throwing an error (include response
status/text or parsed JSON error) so callers know persistence failed; ensure the
promise rejection or error is propagated instead of swallowing the failure so
onboarding UI can react.

---

Nitpick comments:
In `@app/api/account/update/route.tsx`:
- Line 9: Create a TypeScript interface (e.g. UpdateAccountRequest) that
declares accountId: string and the optional fields instruction, name,
organization, image, jobTitle, roleType, companyName, knowledges,
onboardingStatus, and onboardingData with appropriate types (use unknown or
Record<string, unknown> where structure is not fixed), then annotate the request
body with that interface where you parse/destructure body (the variable named
body in the route handler) so the destructured line "const { instruction, name,
organization, accountId, image, jobTitle, roleType, companyName, knowledges,
onboardingStatus, onboardingData } = body;" is type-checked against
UpdateAccountRequest; adjust any downstream usages to satisfy the new types.

In `@components/Onboarding/OnboardingArtistsStep.tsx`:
- Around line 117-138: The artist list uses the array index as the React key
(key={i}) which is unstable; update the list rendering in the component that
maps artists (the artists.map in OnboardingArtistsStep) to use a stable
composite key such as `${a.spotifyUrl ?? a.name.toLowerCase()}` or similar so
keys remain stable across reorders/removals; keep the remove(i) call the same
but ensure each <li> uses that stable identifier instead of i and reference
ArtistAvatar and the a object fields (name, spotifyUrl, imageUrl) when
constructing the key.

In `@components/Onboarding/OnboardingCompleteStep.tsx`:
- Around line 97-98: The decorative emoji span in the OnboardingCompleteStep
component (the <span> rendering item.icon alongside item.text) duplicates the
textual semantics and should be hidden from assistive tech; update the icon span
that renders item.icon to include aria-hidden="true" (or aria-hidden={true}) so
screen readers ignore it while leaving the text span (item.text) unchanged.
- Around line 8-25: Rename the inline Props type to an exported, explicitly
named type OnboardingCompleteStepProps and update the component signature to use
that type: export type OnboardingCompleteStepProps = { artistNames: string[];
name: string | undefined; connectedCount: number; pulseEnabled: boolean;
onComplete: () => void; } and change the function parameter typing in
OnboardingCompleteStep to accept OnboardingCompleteStepProps instead of Props;
ensure the new type is exported so other modules can import it.

In `@components/Onboarding/OnboardingConnectionsStep.tsx`:
- Around line 104-119: The Connect button (Button) should include an accessible
label indicating it opens an external destination for screen readers; update the
JSX where onClick calls handleConnect(c.slug) and isConnecting controls state to
pass an aria-label like `Connect to ${c.slug} (opens external link)` or similar
descriptive text to the Button (or the ExternalLink icon if you prefer the label
on the icon), ensuring the label dynamically uses c.slug and preserving existing
isConnecting and disabled behavior.

In `@components/Onboarding/OnboardingContextStep.tsx`:
- Around line 35-47: Replace the current useEffect with a version that uses a
ref (e.g., hasPrefilledRef) to ensure pre-fill runs only once and avoid the
stale-closure risk: create const hasPrefilledRef = useRef(false), then in the
effect check if (!hasPrefilledRef.current && !name && (userData?.name || email))
{ /* compute inferred same as before */ if (inferred) { onChangeName(inferred);
hasPrefilledRef.current = true } }, and include onChangeName, userData?.name,
email (and optionally name) in the dependency array and remove the
eslint-disable comment so the effect sees updated callback references.

In `@components/Onboarding/OnboardingNavButtons.tsx`:
- Around line 3-8: The props interface OnboardingNavButtonsProps should be
exported so other modules can import and reuse it; update the declaration of the
interface OnboardingNavButtonsProps to be exported (e.g., add the export keyword
before the interface) and ensure any local usages/imports remain valid after
making the interface exportable.

In `@components/Onboarding/onboardingRoleConfig.ts`:
- Around line 70-80: getRoleConfig currently returns an inline "other" object as
a fallback which duplicates the existing role; change it to reference the
canonical entry in ROLE_CONFIG_MAP (e.g., use ROLE_CONFIG_MAP["other"]) as the
default, and if "other" may not always exist add a runtime guard that falls back
to a minimal inline object only when ROLE_CONFIG_MAP["other"] is undefined;
update getRoleConfig to use ROLE_CONFIG_MAP[roleId ?? ""] ??
ROLE_CONFIG_MAP["other"] (with the extra guard if needed).

In `@components/Onboarding/OnboardingStepDots.tsx`:
- Around line 4-11: The STEPS constant contains an unused label field (const
STEPS: { id: OnboardingStep; label: string }[]) in OnboardingStepDots.tsx;
either remove the label property from the STEPS entries and their type to clean
up dead data, or update the OnboardingStepDots component to render the label
(e.g., next to each dot) so the label is used—locate the STEPS array and the
rendering logic in OnboardingStepDots (and the OnboardingStep type) and apply
one of these two changes consistently.
- Around line 23-56: Update the OnboardingStepDots component to expose semantic
ARIA attributes: add role="list" and an appropriate aria-label (e.g.,
"Onboarding progress") to the outer container, set each step wrapper (the div
keyed by step.id) to role="listitem", and on the dot element include
aria-current="step" when isCurrent and a clear accessible name (e.g., the
step.title/step.label via a visually-hidden span or aria-label) so screen
readers know which step is current and what each dot represents; also mark
future steps as aria-disabled="true" or omit aria-current for non-current steps.
Ensure you update the JSX near STEPS.map, currentIdx, and the dot/container
divs, using cn only for visual classes.

In `@components/Onboarding/OnboardingWelcomeStep.tsx`:
- Around line 119-129: The emoji status indicators inside the
OnboardingWelcomeStep (the motion.div that renders {i < lineIdx ? "✅" : "⏳"}
alongside {line}) are decorative but convey state; update that rendering so the
emoji itself is aria-hidden and add an accessible status label for screen
readers—either by adding an aria-label on the motion.div (e.g., "completed" vs
"in progress" based on i and lineIdx) or by inserting a visually-hidden <span>
with that status text next to the emoji; ensure the logic uses the same i and
lineIdx conditions so screen readers receive the current status for each line.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 10231d98-d1ae-4aea-9d01-d7d2a427310c

📥 Commits

Reviewing files that changed from the base of the PR and between 0371429 and 508792c.

📒 Files selected for processing (18)
  • app/api/account/update/route.tsx
  • components/Home/HomePage.tsx
  • components/Onboarding/OnboardingArtistsStep.tsx
  • components/Onboarding/OnboardingCompleteStep.tsx
  • components/Onboarding/OnboardingConfetti.tsx
  • components/Onboarding/OnboardingConnectionsStep.tsx
  • components/Onboarding/OnboardingContextStep.tsx
  • components/Onboarding/OnboardingModal.tsx
  • components/Onboarding/OnboardingNavButtons.tsx
  • components/Onboarding/OnboardingPulseStep.tsx
  • components/Onboarding/OnboardingRoleStep.tsx
  • components/Onboarding/OnboardingStepDots.tsx
  • components/Onboarding/OnboardingTasksStep.tsx
  • components/Onboarding/OnboardingWelcomeStep.tsx
  • components/Onboarding/onboardingRoleConfig.ts
  • components/Onboarding/useOnboarding.ts
  • components/Onboarding/useOnboardingPersist.ts
  • components/Onboarding/useSpotifyArtistSearch.ts

Comment on lines +43 to +57
useEffect(() => {
const interval = setInterval(() => {
setLineIdx(prev => {
const next = prev + 1;
if (next >= RESEARCH_LINES.length) {
clearInterval(interval);
setDone(true);
setTimeout(onDone, 700);
return prev;
}
return next;
});
}, 520);
return () => clearInterval(interval);
}, [onDone]);
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

Potential memory leak: setTimeout may fire after unmount.

The setTimeout(onDone, 700) on line 50 isn't cleared on unmount. If the user closes the modal before the 700ms elapses, onDone will still fire, potentially causing state updates on an unmounted component.

🛡️ Proposed fix to track and clear the timeout
 export function OnboardingWelcomeStep({ onDone }: Props) {
   const { email, userData } = useUserProvider();
   const [lineIdx, setLineIdx] = useState(0);
   const [done, setDone] = useState(false);
+  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

   // ... displayName logic ...

   useEffect(() => {
     const interval = setInterval(() => {
       setLineIdx(prev => {
         const next = prev + 1;
         if (next >= RESEARCH_LINES.length) {
           clearInterval(interval);
           setDone(true);
-          setTimeout(onDone, 700);
+          timeoutRef.current = setTimeout(onDone, 700);
           return prev;
         }
         return next;
       });
     }, 520);
-    return () => clearInterval(interval);
+    return () => {
+      clearInterval(interval);
+      if (timeoutRef.current) clearTimeout(timeoutRef.current);
+    };
   }, [onDone]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingWelcomeStep.tsx` around lines 43 - 57, The
useEffect in OnboardingWelcomeStep sets a setTimeout(onDone, 700) but never
tracks or clears that timeout on unmount; update the effect to store the timeout
ID (e.g. const doneTimer = setTimeout(...)) when scheduling onDone and clear it
in the cleanup function (clearTimeout(doneTimer)), and also clear any pending
doneTimer right before calling setTimeout inside the interval branch to avoid
duplicates; reference the useEffect, setInterval, setTimeout(onDone, 700),
setLineIdx, setDone and onDone symbols when making this change.

- handleComplete: wrap persist() in try/catch to gate complete() and
  redirect on success only
- OnboardingModal: deduplicate connectedSlugs using Set to prevent
  inflated connectedCount from multiple connect events
- OnboardingWelcomeStep: clear the auto-advance setTimeout on unmount
  via useRef to prevent state updates after component is gone
- useOnboardingPersist: check response.ok from /api/account/update and
  throw on 4xx/5xx so persist failures surface to the caller
- OnboardingCompleteStep: add isCompleting state to disable the button
  after first click, preventing duplicate persistence/redirect attempts
- OnboardingConfetti: move borderRadius randomization into makeParticles()
  so particle shapes are stable across re-renders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 (3)
components/Onboarding/OnboardingCompleteStep.tsx (2)

34-51: Consider extracting summary item generation for testability.

The summaryItems construction logic is inline and uses a filter-then-cast pattern. While functional, extracting this to a pure function would improve testability and make the conditional logic more explicit.

♻️ Optional: Extract summary builder
+type SummaryItem = { icon: string; text: string };
+
+function buildSummaryItems(
+  artistNames: string[],
+  connectedCount: number,
+  pulseEnabled: boolean,
+): SummaryItem[] {
+  const items: SummaryItem[] = [];
+  if (artistNames.length > 0) {
+    items.push({
+      icon: "🎤",
+      text: `Deep research running on ${artistNames.slice(0, 2).join(" & ")}${
+        artistNames.length > 2 ? ` +${artistNames.length - 2} more` : ""
+      }`,
+    });
+  }
+  if (connectedCount > 0) {
+    items.push({
+      icon: "🔗",
+      text: `${connectedCount} platform${connectedCount > 1 ? "s" : ""} connected`,
+    });
+  }
+  if (pulseEnabled) {
+    items.push({ icon: "⚡", text: "Pulse active — briefing arrives tomorrow morning" });
+  }
+  items.push(
+    { icon: "✅", text: "First week of tasks queued" },
+    { icon: "🧠", text: "AI learning your artists and fans right now" },
+  );
+  return items;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingCompleteStep.tsx` around lines 34 - 51,
Extract the inline summaryItems logic into a pure function (e.g.,
buildSummaryItems or getSummaryItems) that accepts artistNames: string[],
connectedCount: number, and pulseEnabled: boolean and returns a typed array {
icon: string; text: string }[]; move the conditional item creation out of the
JSX and into that function, avoid the filter(Boolean) + cast pattern by only
pushing valid items to the result array or using a compact map/filter with
proper typing, then replace the inline summaryItems with a call to the new
function so the logic is testable and isolated.

58-63: Minor indentation inconsistency in JSX.

The motion.div starting at line 58 has inconsistent indentation compared to its parent structure. While this doesn't affect functionality, consistent formatting improves readability.

🔧 Fix indentation
         <>
           <OnboardingConfetti />
           <motion.div
-          className="flex flex-col items-center gap-7 text-center"
-          initial={{ opacity: 0, scale: 0.95 }}
-          animate={{ opacity: 1, scale: 1 }}
-          transition={{ duration: 0.4, ease: "easeOut" }}
-        >
+            className="flex flex-col items-center gap-7 text-center"
+            initial={{ opacity: 0, scale: 0.95 }}
+            animate={{ opacity: 1, scale: 1 }}
+            transition={{ duration: 0.4, ease: "easeOut" }}
+          >

Also applies to: 126-126

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

In `@components/Onboarding/OnboardingCompleteStep.tsx` around lines 58 - 63, The
JSX indentation for the motion.div elements in OnboardingCompleteStep (the
<motion.div ...> blocks) is inconsistent with the surrounding JSX; fix by
aligning those motion.div opening and closing tags and their props to the same
indentation level as their sibling JSX elements inside the component (ensure the
className/initial/animate/transition props are indented one level in from the
parent element), and make the same adjustment for the other occurrence of the
motion.div to maintain consistent formatting throughout the component.
components/Onboarding/OnboardingModal.tsx (1)

39-39: Extract magic number to a named constant.

The 200 millisecond delay is a magic number. Extracting it to a named constant improves readability and makes the intent clearer.

♻️ Extract constant
+const REDIRECT_DELAY_MS = 200;
+
 const handleComplete = useCallback(async () => {
   // ...
-  setTimeout(() => { window.location.href = `/?q=${q}`; }, 200);
+  setTimeout(() => { window.location.href = `/?q=${q}`; }, REDIRECT_DELAY_MS);
 }, [complete, persist, data]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingModal.tsx` at line 39, Extract the magic
number 200ms used in the setTimeout inside the OnboardingModal component into a
named constant (e.g., ONBOARDING_REDIRECT_DELAY_MS or REDIRECT_DELAY_MS)
declared at the top of the module (or near the OnboardingModal definition) and
replace the inline literal in setTimeout(() => { window.location.href =
`/?q=${q}`; }, 200) with that constant so the delay is self-documenting and easy
to adjust.
🤖 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/Onboarding/OnboardingModal.tsx`:
- Around line 27-41: The catch block in handleComplete currently swallows
persistence errors; update handleComplete to catch the error object from
persist(data) and show a user-facing toast via sonner (e.g., toast.error or
toast) with a clear message like "Failed to save onboarding — please try again"
and optionally include short error details, then return early (so complete() is
not called); keep the existing flow that calls complete() only after successful
await persist(data) and leaves the artist redirect logic (artistNames, encoded
q, setTimeout -> window.location.href) unchanged.
- Around line 35-40: In OnboardingModal, the redirect behavior is inconsistent:
when artistNames has entries the code builds q and does setTimeout(() => {
window.location.href = `/?q=${q}`; }, 200) but when artistNames is empty the
modal simply closes—either add a concise comment above this block (referencing
artistNames, q, setTimeout, window.location.href) explaining this intentional
difference, or unify behavior by providing a fallback/default query for the
empty-artist path and perform the same redirect pattern (use the same timeout +
window.location.href logic) instead of only closing the modal so both flows
consistently land on HomePage/Chat with a query param.

---

Nitpick comments:
In `@components/Onboarding/OnboardingCompleteStep.tsx`:
- Around line 34-51: Extract the inline summaryItems logic into a pure function
(e.g., buildSummaryItems or getSummaryItems) that accepts artistNames: string[],
connectedCount: number, and pulseEnabled: boolean and returns a typed array {
icon: string; text: string }[]; move the conditional item creation out of the
JSX and into that function, avoid the filter(Boolean) + cast pattern by only
pushing valid items to the result array or using a compact map/filter with
proper typing, then replace the inline summaryItems with a call to the new
function so the logic is testable and isolated.
- Around line 58-63: The JSX indentation for the motion.div elements in
OnboardingCompleteStep (the <motion.div ...> blocks) is inconsistent with the
surrounding JSX; fix by aligning those motion.div opening and closing tags and
their props to the same indentation level as their sibling JSX elements inside
the component (ensure the className/initial/animate/transition props are
indented one level in from the parent element), and make the same adjustment for
the other occurrence of the motion.div to maintain consistent formatting
throughout the component.

In `@components/Onboarding/OnboardingModal.tsx`:
- Line 39: Extract the magic number 200ms used in the setTimeout inside the
OnboardingModal component into a named constant (e.g.,
ONBOARDING_REDIRECT_DELAY_MS or REDIRECT_DELAY_MS) declared at the top of the
module (or near the OnboardingModal definition) and replace the inline literal
in setTimeout(() => { window.location.href = `/?q=${q}`; }, 200) with that
constant so the delay is self-documenting and easy to adjust.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3fc123ab-b481-40ea-8169-612fd1e5afce

📥 Commits

Reviewing files that changed from the base of the PR and between 508792c and c08482b.

📒 Files selected for processing (5)
  • components/Onboarding/OnboardingCompleteStep.tsx
  • components/Onboarding/OnboardingConfetti.tsx
  • components/Onboarding/OnboardingModal.tsx
  • components/Onboarding/OnboardingWelcomeStep.tsx
  • components/Onboarding/useOnboardingPersist.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/Onboarding/useOnboardingPersist.ts
  • components/Onboarding/OnboardingConfetti.tsx
  • components/Onboarding/OnboardingWelcomeStep.tsx

- Defer onConnect until OAuth popup closes to prevent false checkmarks on cancel
- Use functional updater in updateData to avoid stale closure dropping concurrent slugs
- Show sonner toast on persist error instead of silently swallowing it
- Add comment explaining intentional redirect behavior difference (artists vs no artists)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
components/Onboarding/OnboardingConnectionsStep.tsx (1)

61-69: ⚠️ Potential issue | 🟠 Major

Closing the auth window still marks the connector as connected.

Manual close or a canceled OAuth flow will still hit onConnect(slug), so the checkmark is still optimistic. Please only mark the slug connected from a verified callback or a backend status check after the OAuth flow completes.

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

In `@components/Onboarding/OnboardingConnectionsStep.tsx` around lines 61 - 69,
The current logic in the popup OAuth flow unconditionally calls onConnect(slug)
when the popup is closed (see window.open, popup, checkClosed, onConnect,
setConnecting), which marks connectors connected even on user-cancel or failed
OAuth; change this to only mark a connector connected after a confirmed backend
callback or verified status: remove the onConnect(slug) invocation from the
popup.closed branch and instead implement a verification step (e.g., poll a
backend endpoint or listen for a postMessage/webhook confirmation) that checks
connection status for the given slug and only call onConnect(slug) and
setConnecting(null) after that verification succeeds, while ensuring the popup
closed case cancels the spinner and does not optimistically mark the connection.
🤖 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/Onboarding/OnboardingConnectionsStep.tsx`:
- Line 50: The current connecting state (connecting / setConnecting) holds a
single slug so a second OAuth flow overwrites the first and clears the wrong
spinner; fix by making connecting a collection or global flag and updating the
handlers: either change connecting from string|null to Set<string> (or
string[]), call setConnecting.add(slug) when a flow starts and remove the slug
on finish/error (ensure cleanup in the popup close/complete handlers), and
update the Connect button disabled logic to check collection.size>0 or
collection.includes(slug) as appropriate; alternatively, if you want to allow
only one flow at a time, convert connecting to a boolean and disable every
Connect button while connecting is true. Update all usages of connecting and
setConnecting (the click handler that initiates auth, the popup completion
handler, and any UI button disabled checks) to match the chosen approach.
- Around line 75-76: The empty catch in OnboardingConnectionsStep.tsx swallows
errors from authorizeConnectorApi() so users get no feedback; update the catch
to capture the thrown error and display a toast using sonner (e.g., toast.error)
with a clear message including the error.message (or a fallback) so the user
knows connector authorization failed, and then either return/stop further flow
or rethrow as appropriate; ensure you import toast from 'sonner' and reference
authorizeConnectorApi in the catch handling.

In `@components/Onboarding/OnboardingModal.tsx`:
- Around line 52-56: The Dialog currently has no accessible name; add a visible
or hidden heading and reference it from the dialog so screen readers can
announce the title: either include a DialogTitle element inside OnboardingModal
with the human-readable title text (e.g., <DialogTitle>Onboarding</DialogTitle>)
or render a heading element with a stable id (e.g., id="onboarding-dialog-title"
and visually-hidden styling) and pass aria-labelledby="onboarding-dialog-title"
to the DialogContent (or the dialog root) alongside the existing props
(onInteractOutside, onEscapeKeyDown) so assistive tech can identify the dialog.

In `@components/Onboarding/useOnboarding.ts`:
- Around line 46-61: The hook currently only toggles isOpen via complete(), so
stale userData with onboarding_status will reopen the wizard; add a local
per-account completion latch (e.g., a state map keyed by userData.id or
accountId) inside useOnboarding (lookup on mount and set when complete() is
called) or perform an optimistic update to userData via useOnboardingPersist()
so subsequent userData emissions don't reopen the wizard; update the useEffect
that reads userData/onboarding_status to consult this local latch before calling
setIsOpen and ensure complete() writes to the latch and/or updates userData
optimistically.

---

Duplicate comments:
In `@components/Onboarding/OnboardingConnectionsStep.tsx`:
- Around line 61-69: The current logic in the popup OAuth flow unconditionally
calls onConnect(slug) when the popup is closed (see window.open, popup,
checkClosed, onConnect, setConnecting), which marks connectors connected even on
user-cancel or failed OAuth; change this to only mark a connector connected
after a confirmed backend callback or verified status: remove the
onConnect(slug) invocation from the popup.closed branch and instead implement a
verification step (e.g., poll a backend endpoint or listen for a
postMessage/webhook confirmation) that checks connection status for the given
slug and only call onConnect(slug) and setConnecting(null) after that
verification succeeds, while ensuring the popup closed case cancels the spinner
and does not optimistically mark the connection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff6f4489-f2c4-4ff8-94ce-d09bc0c9e6e1

📥 Commits

Reviewing files that changed from the base of the PR and between c08482b and 28ae6db.

📒 Files selected for processing (3)
  • components/Onboarding/OnboardingConnectionsStep.tsx
  • components/Onboarding/OnboardingModal.tsx
  • components/Onboarding/useOnboarding.ts

*/
export function OnboardingConnectionsStep({ connected, onConnect, onNext, onBack }: Props) {
const accessToken = useAccessToken();
const [connecting, setConnecting] = useState<string | null>(null);
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

A second connector flow overwrites the first loading state.

connecting only stores one slug, but only the matching button is disabled. Starting a second auth flow makes the first card look idle and lets popup A clear popup B's spinner when it closes. Either track pending slugs per connector or disable every Connect button while any flow is active.

💡 Minimal fix if only one OAuth flow should run at a time
@@
   const accessToken = useAccessToken();
   const [connecting, setConnecting] = useState<string | null>(null);
+  const isAuthorizing = connecting !== null;
@@
-                  disabled={isConnecting}
+                  disabled={isAuthorizing}

Also applies to: 96-97, 114-118

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

In `@components/Onboarding/OnboardingConnectionsStep.tsx` at line 50, The current
connecting state (connecting / setConnecting) holds a single slug so a second
OAuth flow overwrites the first and clears the wrong spinner; fix by making
connecting a collection or global flag and updating the handlers: either change
connecting from string|null to Set<string> (or string[]), call
setConnecting.add(slug) when a flow starts and remove the slug on finish/error
(ensure cleanup in the popup close/complete handlers), and update the Connect
button disabled logic to check collection.size>0 or collection.includes(slug) as
appropriate; alternatively, if you want to allow only one flow at a time,
convert connecting to a boolean and disable every Connect button while
connecting is true. Update all usages of connecting and setConnecting (the click
handler that initiates auth, the popup completion handler, and any UI button
disabled checks) to match the chosen approach.

Comment on lines +75 to +76
} catch {
// silently continue
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

Surface connector authorization failures.

authorizeConnectorApi() throws on non-2xx responses, and this empty catch drops the user back to idle with no explanation. Toast the failure so they know the auth flow never started.

💡 Proposed fix
@@
 import { useState } from "react";
+import { toast } from "sonner";
@@
-    } catch {
-      // silently continue
+    } catch {
+      toast.error("Couldn't start the connector authorization. Please try again.");
     }
As per coding guidelines, "Use `sonner` for toast notifications".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingConnectionsStep.tsx` around lines 75 - 76,
The empty catch in OnboardingConnectionsStep.tsx swallows errors from
authorizeConnectorApi() so users get no feedback; update the catch to capture
the thrown error and display a toast using sonner (e.g., toast.error) with a
clear message including the error.message (or a fallback) so the user knows
connector authorization failed, and then either return/stop further flow or
rethrow as appropriate; ensure you import toast from 'sonner' and reference
authorizeConnectorApi in the catch handling.

Comment on lines +52 to +56
<DialogContent
className="max-w-lg p-0 gap-0 overflow-hidden [&>button]:hidden"
onInteractOutside={e => e.preventDefault()}
onEscapeKeyDown={e => e.preventDefault()}
>
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

Give the onboarding dialog an accessible name.

This modal opens without a DialogTitle or aria-labelledby, so assistive tech will announce an unnamed dialog. Add a visible title or wire the dialog to a hidden heading.

♿ Proposed fix
@@
       <DialogContent
+        aria-labelledby="onboarding-dialog-title"
         className="max-w-lg p-0 gap-0 overflow-hidden [&>button]:hidden"
         onInteractOutside={e => e.preventDefault()}
         onEscapeKeyDown={e => e.preventDefault()}
       >
+        <h2 id="onboarding-dialog-title" className="sr-only">
+          Recoupable onboarding
+        </h2>
         {isProgressStep && (
As per coding guidelines, "Provide proper ARIA roles/states and test with screen readers".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Onboarding/OnboardingModal.tsx` around lines 52 - 56, The Dialog
currently has no accessible name; add a visible or hidden heading and reference
it from the dialog so screen readers can announce the title: either include a
DialogTitle element inside OnboardingModal with the human-readable title text
(e.g., <DialogTitle>Onboarding</DialogTitle>) or render a heading element with a
stable id (e.g., id="onboarding-dialog-title" and visually-hidden styling) and
pass aria-labelledby="onboarding-dialog-title" to the DialogContent (or the
dialog root) alongside the existing props (onInteractOutside, onEscapeKeyDown)
so assistive tech can identify the dialog.

Comment on lines +46 to +61
const { userData } = useUserProvider();
const [isOpen, setIsOpen] = useState(false);
const [step, setStep] = useState<OnboardingStep>("welcome");
const [data, setData] = useState<Partial<OnboardingData>>({
artists: [],
connectedSlugs: [],
pulseEnabled: false,
});

useEffect(() => {
if (!userData) return;
const status = userData.onboarding_status as Record<string, unknown> | null;
if (!status?.completed) {
setIsOpen(true);
}
}, [userData]);
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

Closing locally is not durable if userData stays stale.

complete() only flips isOpen, so any later userData emission with the old onboarding_status will reopen the wizard. Since useOnboardingPersist() does not refresh userData, this hook needs a local completion latch keyed to the current account, or an optimistic provider update.

💡 One way to latch completion per account
@@
 export function useOnboarding() {
   const { userData } = useUserProvider();
+  const currentAccountKey = userData?.account_id ?? "__missing_account__";
   const [isOpen, setIsOpen] = useState(false);
+  const [completedForAccountKey, setCompletedForAccountKey] = useState<string | null>(null);
   const [step, setStep] = useState<OnboardingStep>("welcome");
   const [data, setData] = useState<Partial<OnboardingData>>({
     artists: [],
     connectedSlugs: [],
     pulseEnabled: false,
   });
 
   useEffect(() => {
-    if (!userData) return;
+    if (!userData || completedForAccountKey === currentAccountKey) return;
     const status = userData.onboarding_status as Record<string, unknown> | null;
-    if (!status?.completed) {
-      setIsOpen(true);
-    }
-  }, [userData]);
+    setIsOpen(!status?.completed);
+  }, [userData, currentAccountKey, completedForAccountKey]);
@@
   const complete = useCallback(() => {
+    setCompletedForAccountKey(currentAccountKey);
     setIsOpen(false);
-  }, []);
+  }, [currentAccountKey]);

Also applies to: 88-90

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

In `@components/Onboarding/useOnboarding.ts` around lines 46 - 61, The hook
currently only toggles isOpen via complete(), so stale userData with
onboarding_status will reopen the wizard; add a local per-account completion
latch (e.g., a state map keyed by userData.id or accountId) inside useOnboarding
(lookup on mount and set when complete() is called) or perform an optimistic
update to userData via useOnboardingPersist() so subsequent userData emissions
don't reopen the wizard; update the useEffect that reads
userData/onboarding_status to consult this local latch before calling setIsOpen
and ensure complete() writes to the latch and/or updates userData
optimistically.

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