Skip to content

feat(onboarding): add artists by Spotify typeahead, not free text (chat#1889 row 8) - #1892

Merged
sweetmantech merged 3 commits into
mainfrom
feat/add-artist-spotify-typeahead
Jul 28, 2026
Merged

feat(onboarding): add artists by Spotify typeahead, not free text (chat#1889 row 8)#1892
sweetmantech merged 3 commits into
mainfrom
feat/add-artist-spotify-typeahead

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Matrix row 8 in chat#1889 — the highest-value item after the convergence chain. Independent of #1890/#1891 (different files), so it can be reviewed in parallel.

Why: the payoff was structurally unreachable for most signups

Live cohort from email_send_log, 2026-07-23 → 2026-07-26:

Welcome emails sent 9, to 9 distinct accounts
With a roster + catalog (valuation-funnel signups) 3
With zero artists, zero catalogs, no valuation 6

The welcome email fires on account creation regardless of whether a valuation preceded it, so two-thirds of the cohort are cold-start signups. Their only forward path is AddArtistForm — which took a free-text name and posted only { name }. No Spotify id → no catalog → no valuation. The number those users were emailed about could never exist.

What changed

  • AddArtistForm uses SpotifyArtistSearch — the same typeahead verify-socials adopted in feat(onboarding): Spotify typeahead in verify-socials (item 3b of #1881) #1882 — so a pick resolves to a real Spotify id, profile URL, and avatar.
  • useAddRosterArtist now takes { profileUrl, image } and attaches them via the existing PATCH /api/artists/{id} path after creation, because POST /api/artists accepts only name / account_id / organization_id. Reuses buildSocialFixPayload for the platform detection rather than hand-rolling the shape. Enrichment failure is deliberately non-fatal — the artist is already on the roster and socials can still be fixed in the next step.
  • ConfirmRosterStep copy: an empty roster was told "We set these artists up from your valuation" — false for 6 of 9 signups. It now reads as an instruction to search, and the empty state names the reward.

No api change needed, so no docs/contract change either — this rides two existing endpoints.

Verification

TDD, red → green:

```

RED — free-text field still present, no typeahead

Test Files 1 failed (1)
Tests 2 failed (2)

GREEN

pnpm exec vitest run components/Onboarding lib/onboarding
Test Files 18 passed (18)
Tests 70 passed (70)
```

The new test asserts the free-text Artist name field is gone, the Spotify searchbox is present, and picking a result calls addArtist with the resolved profile URL + avatar.

pnpm exec tsc --noEmit clean for the touched files. Preview click-through pending (needs an authed empty-roster account).

Tracked in chat#1889 (matrix row 8).


Summary by cubic

Replace free-text artist input with a Spotify typeahead in onboarding so added artists have a real Spotify profile and avatar. This unblocks cold-start signups so they can reach catalog and valuation (chat#1889, row 8).

  • New Features

    • AddArtistForm now uses SpotifyArtistSearch; selecting a result calls addArtist(name, { profileUrl, image }).
    • useAddRosterArtist attaches the Spotify profile and image via PATCH /api/artists/{id} using buildSocialFixPayload, then refreshes the roster.
    • ConfirmRosterStep copy prompts Spotify search when empty and only credits a valuation if the account actually has a catalog.
  • Bug Fixes

    • Enrichment failures are non-fatal: saveArtist errors are caught so the form closes and the roster refreshes, avoiding duplicates.
    • Removed em dashes from roster and socials copy (including button text) to improve tone; enforced via getConfirmRosterCopy tests.

Written for commit 617d7f9. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added Spotify artist search for selecting artists during onboarding.
    • When available, automatically attaches an artist’s Spotify profile link and artwork.
    • Updated the “Confirm your roster” message to be tailored to the number of artists and whether valuation is available.
  • Bug Fixes
    • Artist additions now retain available Spotify profile and artwork details, and the flow closes automatically after a successful add.

@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview Jul 28, 2026 4:10pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 32b916fa-4aec-4030-85e1-45974709b5ce

📥 Commits

Reviewing files that changed from the base of the PR and between fa7a630 and 617d7f9.

⛔ Files ignored due to path filters (1)
  • lib/onboarding/__tests__/getConfirmRosterCopy.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (3)
  • components/Onboarding/ConfirmRosterStep.tsx
  • components/Onboarding/VerifySocialsStep.tsx
  • lib/onboarding/getConfirmRosterCopy.ts
📝 Walkthrough

Walkthrough

AddArtistForm now selects artists from Spotify and passes profile and image metadata into roster creation. The roster-add hook conditionally persists that enrichment, while confirmation copy varies by roster size and catalog valuation state.

Changes

Roster artist onboarding

Layer / File(s) Summary
Roster confirmation copy contract
lib/onboarding/getConfirmRosterCopy.ts
Adds a typed copy generator for empty rosters, additional artists, and valuation provenance with singular/plural wording.
Spotify artist selection and enrichment
components/Onboarding/AddArtistForm.tsx, hooks/onboarding/useAddRosterArtist.ts
Replaces free-text entry with Spotify selection, forwards profile and image metadata, and conditionally saves enrichment after artist creation.
Valuation-aware confirmation integration
components/Onboarding/ConfirmRosterStep.tsx
Loads catalog state and renders confirmation guidance based on artist count and whether a valuation exists.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AddArtistForm
  participant SpotifyArtistSearch
  participant useAddRosterArtist
  participant createRosterArtist
  participant saveArtist
  AddArtistForm->>SpotifyArtistSearch: register onSelect and isBusy
  SpotifyArtistSearch->>AddArtistForm: return selected Spotify artist
  AddArtistForm->>useAddRosterArtist: addArtist(name, profileUrl, image)
  useAddRosterArtist->>createRosterArtist: create roster artist
  createRosterArtist-->>useAddRosterArtist: return account_id
  useAddRosterArtist->>saveArtist: persist social and avatar enrichment
Loading

Possibly related PRs

  • recoupable/chat#1870: Modifies the same onboarding artist-add flow and roster creation behavior.
  • recoupable/chat#1878: Uses Spotify artist selection and enriches created artists with Spotify metadata.

Poem

Spotify stars now fill the form,
Profiles and portraits safely warm.
Empty rosters learn the way,
Add one artist, start the day.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning useAddRosterArtist duplicates the social-fix/auth/refresh flow from useSocialFix and bundles creation, enrichment, toast, and refresh into one ~50-line function. Extract a shared artist-update helper (or reuse useSocialFix) and split the hook into smaller create/enrich/refresh functions so each unit has one responsibility.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-artist-spotify-typeahead

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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a332e38fe7

ℹ️ 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".

Comment on lines +24 to +27
const added = await addArtist(artist.name, {
profileUrl: artist.external_urls.spotify,
image: artist.images?.[0]?.url,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Seed a valuation from the selected Spotify artist

For a cold-start account with no catalogs, this selection passes only the artist name, profile URL, and image, discarding artist.id; the hook then performs only the artist POST/PATCH. Unlike hooks/useAddSpotifyArtist.ts, it never calls runValuation(accessToken, artist.id), so /setup/catalog can still redirect these users to a catalogs page that says "No catalogs found" despite the new copy promising that adding the artist gets the catalog valued.

Useful? React with 👍 / 👎.

Comment thread hooks/onboarding/useAddRosterArtist.ts Outdated
Comment on lines +58 to +61
await saveArtist(accessToken, artist.account_id, {
...(payload ? { profileUrls: payload.profileUrls } : {}),
...(options.image ? { image: options.image } : {}),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat post-create enrichment failures as successful adds

When the initial artist POST succeeds but this PATCH fails (for example, during a transient API/network error), control reaches the outer catch, skips getArtists(), and returns false. The form therefore remains open and invites the user to select the artist again, issuing another POST and potentially creating duplicate roster entries even though the first artist already exists; isolate enrichment failure from creation and still refresh/close after a successful create.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 4 files

Confidence score: 2/5

  • In components/Onboarding/AddArtistForm.tsx, adding the first artist to an empty roster does not trigger catalog valuation, so onboarding can advance without the valuation step users were promised; this is a concrete flow regression with clear user impact — update the add-artist handler to start valuation when the first artist.id is selected and add a regression test for the empty-roster path.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="components/Onboarding/AddArtistForm.tsx">

<violation number="1" location="components/Onboarding/AddArtistForm.tsx:24">
P1: Adding the first artist from an empty onboarding roster still never starts catalog valuation, so users can continue past this step without the catalog the new copy promises. This handler has the selected `artist.id`; route it through `useAddSpotifyArtist` (or trigger the same first-roster valuation flow here) rather than the name/profile-only hook.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread hooks/onboarding/useAddRosterArtist.ts
setIsOpen(false);
}
const handleSelect = async (artist: SpotifyArtistSearchResult) => {
const added = await addArtist(artist.name, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Adding the first artist from an empty onboarding roster still never starts catalog valuation, so users can continue past this step without the catalog the new copy promises. This handler has the selected artist.id; route it through useAddSpotifyArtist (or trigger the same first-roster valuation flow here) rather than the name/profile-only hook.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Onboarding/AddArtistForm.tsx, line 24:

<comment>Adding the first artist from an empty onboarding roster still never starts catalog valuation, so users can continue past this step without the catalog the new copy promises. This handler has the selected `artist.id`; route it through `useAddSpotifyArtist` (or trigger the same first-roster valuation flow here) rather than the name/profile-only hook.</comment>

<file context>
@@ -1,24 +1,31 @@
-      setIsOpen(false);
-    }
+  const handleSelect = async (artist: SpotifyArtistSearchResult) => {
+    const added = await addArtist(artist.name, {
+      profileUrl: artist.external_urls.spotify,
+      image: artist.images?.[0]?.url,
</file context>

chat#1889 matrix row 8 — the highest-value item after convergence.

AddArtistForm took a free-text name and useAddRosterArtist posted only that, so
an added artist had no Spotify id. Without one there is no catalog and no
valuation, which makes the payoff structurally unreachable — and that is the
only forward path for the cold-start cohort: of the 9 accounts that received a
welcome email 2026-07-23..25, 6 had zero artists and had never run a valuation.

- AddArtistForm uses the existing SpotifyArtistSearch typeahead (same component
  verify-socials adopted in #1882), so a pick resolves to a real Spotify id.
- useAddRosterArtist attaches the profile URL (via buildSocialFixPayload) and
  the Spotify avatar through PATCH /api/artists/{id} after creation, since
  POST /api/artists accepts only a name. Enrichment failure is non-fatal.
- ConfirmRosterStep no longer tells an empty roster we set it up "from your
  valuation" — false for two thirds of signups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sweetmantech
sweetmantech force-pushed the feat/add-artist-spotify-typeahead branch from a332e38 to dae3741 Compare July 28, 2026 13:18

@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

🧹 Nitpick comments (1)
components/Onboarding/AddArtistForm.tsx (1)

10-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim implementation-history comments.

These blocks repeat product history and internal chat references rather than documenting non-obvious behavior.

  • components/Onboarding/AddArtistForm.tsx#L10-L18: replace with a brief description of Spotify selection supplying roster metadata.
  • components/Onboarding/ConfirmRosterStep.tsx#L24-L26: remove the comment; the conditional copy is self-explanatory.
  • hooks/onboarding/useAddRosterArtist.ts#L19-L29: retain only the concise API contract and non-fatal enrichment rationale.

As per coding guidelines, “Add short comments only when necessary to clarify non-obvious logic” and “Write minimal code - use only the absolute minimum code needed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Onboarding/AddArtistForm.tsx` around lines 10 - 18, Trim
implementation-history comments across the onboarding roster flow: in
components/Onboarding/AddArtistForm.tsx lines 10-18, replace the history and
chat references with a brief description that Spotify selection supplies roster
metadata; in components/Onboarding/ConfirmRosterStep.tsx lines 24-26, remove the
self-explanatory conditional-copy comment; in
hooks/onboarding/useAddRosterArtist.ts lines 19-29, retain only the concise API
contract and rationale for non-fatal enrichment.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hooks/onboarding/useAddRosterArtist.ts`:
- Around line 54-62: Update the enrichment save block in useAddRosterArtist
around saveArtist so its rejection is handled locally rather than reaching the
outer artist-creation catch. Show an enrichment-specific error message, then
refresh the roster and return success after the artist has already been created;
preserve normal success behavior when enrichment succeeds and avoid treating the
artist creation as failed.

---

Nitpick comments:
In `@components/Onboarding/AddArtistForm.tsx`:
- Around line 10-18: Trim implementation-history comments across the onboarding
roster flow: in components/Onboarding/AddArtistForm.tsx lines 10-18, replace the
history and chat references with a brief description that Spotify selection
supplies roster metadata; in components/Onboarding/ConfirmRosterStep.tsx lines
24-26, remove the self-explanatory conditional-copy comment; in
hooks/onboarding/useAddRosterArtist.ts lines 19-29, retain only the concise API
contract and rationale for non-fatal enrichment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 113c845a-1cb5-40e8-932d-e36c9478282e

📥 Commits

Reviewing files that changed from the base of the PR and between 830b6eb and dae3741.

⛔ Files ignored due to path filters (1)
  • components/Onboarding/__tests__/AddArtistForm.test.tsx is excluded by !**/*.test.* and included by components/**
📒 Files selected for processing (3)
  • components/Onboarding/AddArtistForm.tsx
  • components/Onboarding/ConfirmRosterStep.tsx
  • hooks/onboarding/useAddRosterArtist.ts

Comment on lines +54 to +62
const payload = options.profileUrl
? buildSocialFixPayload(options.profileUrl)
: null;
if (artist?.account_id && (payload || options.image)) {
await saveArtist(accessToken, artist.account_id, {
...(payload ? { profileUrls: payload.profileUrls } : {}),
...(options.image ? { image: options.image } : {}),
});
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep Spotify enrichment failures non-fatal.

A saveArtist rejection reaches the outer catch, so a successfully created artist is reported as failed and the roster is not refreshed. The form remains open, and retrying can create a duplicate artist. Catch only the PATCH failure, show an enrichment-specific message, then refresh and return success.

Proposed fix
       if (artist?.account_id && (payload || options.image)) {
-        await saveArtist(accessToken, artist.account_id, {
-          ...(payload ? { profileUrls: payload.profileUrls } : {}),
-          ...(options.image ? { image: options.image } : {}),
-        });
+        try {
+          await saveArtist(accessToken, artist.account_id, {
+            ...(payload ? { profileUrls: payload.profileUrls } : {}),
+            ...(options.image ? { image: options.image } : {}),
+          });
+        } catch {
+          toast.error("Artist added, but Spotify details could not be saved");
+        }
       }
📝 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 payload = options.profileUrl
? buildSocialFixPayload(options.profileUrl)
: null;
if (artist?.account_id && (payload || options.image)) {
await saveArtist(accessToken, artist.account_id, {
...(payload ? { profileUrls: payload.profileUrls } : {}),
...(options.image ? { image: options.image } : {}),
});
}
const payload = options.profileUrl
? buildSocialFixPayload(options.profileUrl)
: null;
if (artist?.account_id && (payload || options.image)) {
try {
await saveArtist(accessToken, artist.account_id, {
...(payload ? { profileUrls: payload.profileUrls } : {}),
...(options.image ? { image: options.image } : {}),
});
} catch {
toast.error("Artist added, but Spotify details could not be saved");
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/onboarding/useAddRosterArtist.ts` around lines 54 - 62, Update the
enrichment save block in useAddRosterArtist around saveArtist so its rejection
is handled locally rather than reaching the outer artist-creation catch. Show an
enrichment-specific error message, then refresh the roster and return success
after the artist has already been created; preserve normal success behavior when
enrichment succeeds and avoid treating the artist creation as failed.

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Preview verification — rebased onto main, verified end-to-end

Rebased off main (the branch was 3 behind: #1887, #1886, #1890) and force-pushed as dae37413. Zero file overlap with those three, so the rebase was clean and the diff is unchanged at +129/−31 across 4 files.

  • Preview: https://chat-mh0bwz0gc-recoup.vercel.app (deployment 5640583931, built from dae37413)
  • Tests: pnpm exec vitest run components/Onboarding lib/onboarding73 passed / 19 files (was 70/18 pre-rebase; main contributed getOnboardingStepTitle + getSetupPathForStep)
  • Types: pnpm exec tsc --noEmit13 errors on the branch, 13 on main — measured on both, so this PR adds none
  • Account: fresh signup sweetman+july2820260924@recoupable.com, account 57f79840-2c71-4da3-bd00-f916df6054eb — a genuine zero-artist, zero-catalog, never-valued cold start, i.e. the exact 6-of-9 cohort this PR targets

Results

# Check Expected Actual
1 Free-text Artist name input gone no such node in the a11y tree
2 Replacement control Spotify typeahead textbox "Search Spotify for an artist" + listbox "Spotify artist results"
3 Add submit button gone, Cancel remains exactly that
4 Empty-roster copy new string "Search Spotify for the artists you manage… pick the real profile."
5 Empty-state copy new string "No artists on your roster yet. Add your first artist below to get your catalog valued."
6 Typeahead results real Spotify artists 20 results w/ avatars + follower counts via GET /api/spotify/search
7 Fields the PR reads populated external_urls.spotify = …/artist/43qxAkuKFB6fMNSeS5dO7Z, images[0].url = i.scdn.co/…f04af436, followers.total = 1,797,355
8 Create call POST /api/artists 201
9 Enrichment call PATCH /api/artists/{id} 200
10 PATCH payload profileUrls + image {"profileUrls":{"SPOTIFY":"https://open.spotify.com/artist/43qxAkuKFB6fMNSeS5dO7Z"},"image":"https://i.scdn.co/image/ab6761610000e5ebf04af436ecc9ce9b74d715eb"}
11 Spotify id persisted yes account_socials[0].profile_url = open.spotify.com/artist/43qxAkuKFB6fMNSeS5dO7Z, type: SPOTIFY
12 Roster row avatar Spotify image renders; artist.image set to the scdn URL
13 Form collapses on success setIsOpen(false) collapsed, roster refreshed to 1 artist

The headline acceptance criterion — "an artist added during setup carries a Spotify id" — is met. Verified at the data layer, not just visually: GET /api/artists returns the id 43qxAkuKFB6fMNSeS5dO7Z on the created artist.

Deliberate negative probes against the enrichment endpoint:

Probe Result
PATCH /api/artists/{id} no auth 401 Exactly one of x-api-key or Authorization must be provided
PATCH /api/artists/not-a-uuid 400 id must be a valid UUID
PATCH /api/artists/{unowned-uuid} 403 Forbidden

Screenshots

Cold-start empty roster (authenticated) — both new strings
authed empty roster

Typeahead with live Spotify results
typeahead results

After selection — avatar + "1 social profile matched"
artist added

Verify-socials step
verify socials


🔴 One defect, in scope — enrichment failure is not actually non-fatal

The docstring on useAddRosterArtist claims:

Enrichment failing is non-fatal — the artist is already on the roster and its socials can still be fixed in the next step.

The code doesn't do that. await saveArtist(...) sits inside the same try as createRosterArtist, so a PATCH failure lands in the shared catch:

const artist = await createRosterArtist(accessToken, trimmed, selectedOrgId);
if (artist?.account_id && (payload || options.image)) {
  await saveArtist(accessToken, artist.account_id, {...});   // throws ⇒ shared catch
}
await getArtists();      // ← never runs
return true;             // ← never runs

On a PATCH failure the user gets toast.error(...), addArtist returns false, so setIsOpen(false) never fires and getArtists() never refreshes — but POST /api/artists already succeeded. Net effect: the artist exists server-side, the UI says "Failed to add artist" and keeps the form open, and the obvious user response (pick again) creates a duplicate.

This isn't hypothetical — the probes above show the PATCH has three live failure modes (401 on a stale token, 400, 403), plus ordinary network flake. The 403 path is worth weighing given the connector-authority hardening in #1860 / #1865.

Fix is small and keeps the docstring honest:

if (artist?.account_id && (payload || options.image)) {
  try {
    await saveArtist(accessToken, artist.account_id, {
      ...(payload ? { profileUrls: payload.profileUrls } : {}),
      ...(options.image ? { image: options.image } : {}),
    });
  } catch {
    // Non-fatal by design: the artist is created and its socials are
    // fixable in verify-socials. Swallow so the roster still refreshes.
  }
}

Worth a test pinning it — "PATCH rejects ⇒ addArtist still resolves true and the roster refreshes" — since the current suite doesn't cover the partial-failure path.

🟡 The copy fix only covers the zero-artist branch

ConfirmRosterStep was changed for artists.length === 0. But as soon as the cold-start user adds their first artist through this PR's own typeahead, the ternary falls through to the original string and they're told:

"We set this artist up from your valuation. Add anyone else you manage — you can verify their socials next."

on an account that has never run a valuation. (Visible in the third screenshot above — that's the same fresh account, one artist in.) The PR body lists "fixes the false 'from your valuation' copy" as a goal, and it's half-fixed: false at 0 artists is gone, false at ≥1 remains. A hasValuation / catalog-count condition rather than a roster-length one would settle it.

⚪ Pre-existing, not a regression — @artist · 0 followers (#1897)

Verify-socials renders Spotify @artist · 0 followers for an artist with 1.8M followers, with a fallback initial instead of the social avatar. The PATCH sends a correct absolute profile URL; the api derives username from a URL path segment and stores followerCount: 0, avatar: null.

This is the gap #1886 flagged and #1897 owns — this is the fourth independent sighting, and the first confirmed through the new typeahead path, which reinforces the note on #1886 that #1897's scope must cover the handle, not just pfp + follower count. Note the artist-level image is set correctly by this PR; it's only the social row that's bare.

⚪ Scope note — the gate is removed, the payoff is not yet reached

Completing the flow leaves the account at "Roster verified" with GET /api/accounts/{id}/catalogs{"catalogs": []} and /catalogs rendering "No catalogs found." So the chain is now unblocked (the Spotify id exists, which was the hard blocker) but nothing in the setup flow creates the catalog or shows a valuation — the flow is still "Step 1 of 2" and ends on the generic green check.

That's chat#1895 (row 7, valuation reward) and row 8 (persist est_catalog_value), not this PR. Recording it so row 6 isn't mistaken for delivering the payoff on its own.

no catalogs


Not verified

  • The enrichment-failure path was probed at the endpoint level but not forced through the UI, so the 🔴 above is read from the code plus live failure modes rather than observed in-browser.
  • No bot review exists on this PR: CodeRabbit hit the org review limit ("next review available in 37 minutes") at 04:48 on 07-27 and never re-ran; Bugbot is disabled on the account.

Recommendation

Core change works and is worth landing. I'd fix the 🔴 before merge (few lines + one test) and decide on the 🟡 — either widen the condition here or split it out. Verified on dae37413.

…tion that never ran

Two defects found verifying chat#1892 against its preview.

1. saveArtist sat inside the same try as createRosterArtist, so a failing
   PATCH /api/artists/{id} took the shared catch: error toast, addArtist
   returned false, the form stayed open and the roster never refreshed --
   over an artist POST /api/artists had already created. Picking again
   made a duplicate. The docstring already promised this was non-fatal;
   now it is.

2. The copy fix only covered the empty roster. A cold-start account was
   told 'We set this artist up from your valuation' the moment it added
   its first artist through the new typeahead. getConfirmRosterCopy keys
   the claim on a catalog existing rather than on roster length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/Onboarding/ConfirmRosterStep.tsx`:
- Around line 18-20: Update the catalog state handling near useCatalogs and
hasValuation so data === undefined remains an unresolved state rather than being
treated as no valuation. Defer the valuation-specific copy until catalogsData is
known, while preserving the existing no-valuation behavior for a resolved empty
catalogs list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a841ba04-1f76-45cf-ba87-b8bcaa0ec578

📥 Commits

Reviewing files that changed from the base of the PR and between dae3741 and fa7a630.

⛔ Files ignored due to path filters (2)
  • hooks/onboarding/__tests__/useAddRosterArtist.test.tsx is excluded by !**/*.test.* and included by hooks/**
  • lib/onboarding/__tests__/getConfirmRosterCopy.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (3)
  • components/Onboarding/ConfirmRosterStep.tsx
  • hooks/onboarding/useAddRosterArtist.ts
  • lib/onboarding/getConfirmRosterCopy.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • hooks/onboarding/useAddRosterArtist.ts

Comment on lines +18 to +20
const { data: catalogsData } = useCatalogs();
const artists = sorted.filter((artist) => !artist.isWorkspace);
const hasValuation = (catalogsData?.catalogs?.length ?? 0) > 0;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
fd -a 'ConfirmRosterStep\.tsx|.*catalogs.*|useCatalogs.*' . | sed 's#^\./##' | head -100

echo
echo "== ConfirmRosterStep outline =="
if [ -f components/Onboarding/ConfirmRosterStep.tsx ]; then
  wc -l components/Onboarding/ConfirmRosterStep.tsx
  ast-grep outline components/Onboarding/ConfirmRosterStep.tsx || true
  echo
  cat -n components/Onboarding/ConfirmRosterStep.tsx | sed -n '1,260p'
fi

echo
echo "== useCatalogs definitions/usages =="
rg -n "useCatalogs|type .*Catalog|interface .*Catalog|catalogsData|hasValuation" -S .

Repository: recoupable/chat

Length of output: 10705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== useCatalogs.ts =="
cat -n hooks/useCatalogs.ts | sed -n '1,120p'

echo
echo "== ConfirmRosterCopy =="
cat -n lib/onboarding/getConfirmRosterCopy.ts | sed -n '1,120p'

echo
echo "== Query behavior from source =="
rg -n "UseQueryResult|UseSuspense|staleTime|initialData|suspend|error\\b|isLoading\\b|isFetched\\b|refetchOnMount" -S hooks/lib components | head -200

Repository: recoupable/chat

Length of output: 21063


Preserve the unresolved catalog state.

useCatalogs can render with data === undefined before the catalog query resolves, and hasValuation then treats that as false. A populated roster can briefly show “Add anyone else you manage” instead of “We set [subject] up from your valuation.” Distinguish loading from “no valuation” and defer this copy until catalog state is known.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Onboarding/ConfirmRosterStep.tsx` around lines 18 - 20, Update the
catalog state handling near useCatalogs and hasValuation so data === undefined
remains an unresolved state rather than being treated as no valuation. Defer the
valuation-specific copy until catalogsData is known, while preserving the
existing no-valuation behavior for a resolved empty catalogs list.

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Both findings fixed — fa7a630e

Pushed on top of the rebase. TDD both, RED confirmed before any implementation.

🔴 Enrichment is now genuinely non-fatal

saveArtist moved into its own try/catch inside the existing block, so a failing PATCH /api/artists/{id} no longer takes the shared catch.

if (artist?.account_id && (payload || options.image)) {
  try {
    await saveArtist(accessToken, artist.account_id, { ... });
  } catch {
    // Swallowed on purpose. POST /api/artists already succeeded, so
    // surfacing this would leave the form open over an artist that
    // exists — and picking again would create a duplicate. The socials
    // are still fixable in the next step.
  }
}

New hooks/onboarding/__tests__/useAddRosterArtist.test.tsx (the hook had no test file at all) pins three behaviours:

Test Asserts
enriches a created artist saveArtist called with {profileUrls: {SPOTIFY: …}, image: …}, roster refreshed
treats a failed enrichment as non-fatal saveArtist rejects ⇒ addArtist still resolves true, getArtists() still runs, no error toast
still reports failure when the artist itself can't be created createRosterArtist rejects ⇒ resolves false, toast fires, getArtists() not called

The middle one is the regression guard. RED output before the fix, which is the defect reproduced:

FAIL  hooks/onboarding/__tests__/useAddRosterArtist.test.tsx > treats a failed enrichment as non-fatal
AssertionError: expected false to be true
  - Expected: true
  + Received: false

The third test matters as much as the second — it proves the new catch didn't swallow real create failures along with enrichment ones.

🟡 The valuation credit is now keyed on a valuation, not on roster length

Copy logic extracted to a pure lib/onboarding/getConfirmRosterCopy.ts and keyed on hasValuation (derived from useCatalogs() — a catalog is the artifact a valuation leaves behind) rather than artists.length:

Roster Valuation ran Copy
0 artists "Search Spotify for the artists you manage…"
≥1 yes "We set {this artist/these artists} up from your valuation. Add anyone else you manage — you can verify their socials next."
≥1 no "Add anyone else you manage — you can verify their socials next."

Pulling it out of the JSX means the three branches are unit-testable without rendering, matching how getOnboardingStepTitle / getSetupPathForStep already live in lib/onboarding/. The test asserts the negative directly — for rosters of 1, 2 and 5 with no valuation, the string must not match /valuation/i.

Verified on preview chat-hpp1arx60-recoup.vercel.app (built from fa7a630e)

Re-checked on the same account from the first passsweetman+july2820260924@recoupable.com, now sitting at 1 artist / 0 catalogs, which is precisely the state that produced the false claim. Same URL, same account, before and after:

Copy rendered
dae37413 (before) "We set this artist up from your valuation. Add anyone else you manage — you can verify their socials next."
fa7a630e (after) "Add anyone else you manage — you can verify their socials next."

copy fixed

Suite

pnpm exec vitest run components/Onboarding lib/onboarding hooks/onboarding
  Test Files  21 passed (21)
       Tests  79 passed (79)

Up from 73/19 at the start of this pass. pnpm exec tsc --noEmit13 errors, identical to main's 13; pnpm exec eslint clean on all five touched files.

Still not verified

  • The hasValuation: true branch is unit-tested but not browser-confirmed — this account has no catalog, and creating one to prove a copy string wasn't worth the side effects. The funnel-signup path renders it today on main, unchanged by this PR.
  • The @artist · 0 followers gap is untouched here and, per the audit below, has no PR fixing it — chat#1897 explicitly scopes it out ("needs an api resolve path that does not exist yet"), and there is no api PR. It's being given its own row in chat#1889 rather than staying a parenthetical inside row 17.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 5 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

Em dashes read as machine-written and cost trust, so the roster and
socials steps use periods or commas instead. A unit test pins it for
getConfirmRosterCopy so it cannot creep back.

The /setup/tasks strings (FirstTaskStep, buildFirstTaskParams) carry the
same problem but are left alone here: chat#1898 is editing FirstTaskStep
and would conflict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 4 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Copy pass + in-browser verification of both fixes — 617d7f93

Em dashes read as machine-written and cost trust, so they're out of the setup copy. Verified on preview chat-b94uye04u-recoup.vercel.app, built from 617d7f93, on the same cold-start account as the previous passes (sweetman+july2820260924@recoupable.com).

Copy changes

Where Before After
getConfirmRosterCopy tail "Add anyone else you manage you can verify their socials next." "Add anyone else you manage**,** you can verify their socials next."
ConfirmRosterStep CTA "This is my artist continue" "This is my artist**,** continue"
VerifySocialsStep sub-head "…point at the wrong account reports and tasks pull from them." "…point at the wrong account**.** Reports and tasks pull from them."
VerifySocialsStep CTA "Looks good continue" "Looks good**,** continue"

A unit test now pins it, so it can't creep back:

it("uses no em or en dashes in any branch", () => {
  for (const hasValuation of [true, false])
    for (const artistCount of [0, 1, 2, 5])
      expect(getConfirmRosterCopy({ artistCount, hasValuation })).not.toMatch(/[]/);
});

Asserted live rather than by eye — scanning the rendered innerText of both steps for /[—–]/ returns zero matches on each.

Roster step — the copy line, and the singular CTA:

roster copy

Socials step — sub-head and CTA:

socials copy

🔴 The non-fatal enrichment fix, now verified in-browser

The previous comment listed this as read-from-code rather than observed, since the failure couldn't be triggered through the UI. Closing that gap: window.fetch was patched in the page to force exactly the 403 the live endpoint returns for an unowned id on any PATCH /api/artists/*, then an artist was added through the typeahead.

Forced failure confirmed intercepted:

PATCH https://test-recoup-api.vercel.app/api/artists/c2425574-7306-4f4b-b1b9-cce8ea20d6a9  →  403 Forbidden (injected)
Assertion Result
Artist appears on the roster (getArtists() ran) ✅ "Selena" rendered
Add form collapsed (addArtist resolved true) formStillOpen: false
No error toast ✅ no "Failed to add artist" anywhere in the DOM
Degrades honestly ✅ row reads "No socials matched yet" — fixable in the next step, exactly as the docstring promises
Plural CTA branch ✅ "These are my artists, continue"

On dae37413 this same sequence produced "Failed to add artist", left the form open, and left Selena off the roster despite POST /api/artists having already created her — the duplicate trap. It now degrades to an un-enriched roster row, which is the documented contract.

non-fatal enrichment

Suite

pnpm exec vitest run components/Onboarding lib/onboarding hooks/onboarding
  Test Files  21 passed (21)
       Tests  80 passed (80)

pnpm exec tsc --noEmit13, unchanged from main. eslint clean on all touched files.

Deliberately left alone

FirstTaskStep.tsx L45 and buildFirstTaskParams.ts L26 ("Weekly Catalog Report — {artist}") carry the same em dash. Not fixed here because chat#1898 is editing FirstTaskStep.tsx and this would conflict with it. Confirmed safe to change on its own terms — findExistingWeeklyReportTask matches on task.enabled, not on the title — so it's a clean pickup for #1898 or a follow-up. Everything else matching under components/Onboarding/ is code comments, not product text.

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