Skip to content

fix(catalog): cross-account copy stops promising the songs tab (chat#1912 row 6) - #1917

Merged
sweetmantech merged 2 commits into
mainfrom
fix/cross-account-copy-after-auth
Jul 30, 2026
Merged

fix(catalog): cross-account copy stops promising the songs tab (chat#1912 row 6)#1917
sweetmantech merged 2 commits into
mainfrom
fix/cross-account-copy-after-auth

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Third and final link of row 6's chain in chat#1912.

Merge order: docs#282api#802 → this. It is safe to merge at any point (the new wording is true before and after), but it only becomes necessary once api#802 lands.

Why

chat#1915 shipped this copy for a cross-account viewer:

This catalog was measured by another account.
The songs are listed under Manage songs, but the play counts and valuation belong to the account that measured them.

That sentence was written to be true either way while the access model was undecided — at the time, GET /api/catalogs/songs really did return 200 to anyone. api#802 makes it 403 for a non-owner, so the copy would be pointing a stranger at a tab that now fails.

What changed

- The songs are listed under Manage songs, but the play counts and valuation
- belong to the account that measured them.
+ Its play counts and valuation belong to the account that measured it. Run a
+ valuation on your own catalog to see this report.

Same title, same Go to your catalogs CTA. The replacement states only what remains true and offers a next step instead of a dead reference.

Tests

TDD red→green: a new case asserts the other-account body does not mention "Manage songs", so this cannot silently regress if the copy is edited again later. The existing guards in that file still pass unchanged — no recoupable.dev in any state's copy or CTA, and no em dashes.

Full suite 333 passed / 82 files, tsc --noEmit and eslint clean.

Verification

Nothing to verify on a preview until api#802 is merged — before that, the songs tab still returns 200 and both wordings are accurate. Row 6's Works-when step 4 ("re-read the other-account wording and confirm it does not describe behaviour the API no longer has") covers this once the chain is complete.

🤖 Generated with Claude Code


Summary by cubic

Rewords the cross-account catalog report empty state so it no longer points to “Manage songs”. Adds an adaptive CTA that keeps viewers in-app with a clear next step.

  • Bug Fixes
    • Updated body to: "Its play counts and valuation belong to the account that measured it. Here is how to get this report for your own music." CTA adapts: has catalogs → "Go to your catalogs" (/catalogs); none → "Value your catalog" (/setup/artists).
    • Added hasOwnCatalogs to useOwnsCataloguseCatalogReportCatalogReportEmptyState, and tests to guard wording, ensure cross-account always has a CTA, and keep measuring with no CTA.

Written for commit e781cd6. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved the report empty state for another-account views, tailoring the call-to-action based on whether the viewer has their own catalogs.
    • Updated guidance so users are directed to the correct valuation flow to view a report for their own music when applicable.
    • Clarified in the empty-state messaging how play counts and valuation relate to the measuring account.

…1912 row 6)

Third link of the row 6 chain, after recoupable/docs#282 (contract) and
recoupable/api#802 (enforcement).

The other-account state said "The songs are listed under Manage songs, but
the play counts and valuation belong to the account that measured them."
Once catalog songs are account-scoped, that tab returns 403 for a non-owner,
so the copy would be pointing them at something that now fails.

Rewords to state what is true either way and offers an in-app next step
instead. The existing guards still hold: no em dashes, and no CTA off to
recoupable.dev.

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

cursor Bot commented Jul 30, 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 30, 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 30, 2026 9:38pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The catalog report empty state now receives the viewer’s catalog ownership status. For other-account reports, it updates the guidance and directs viewers without catalogs to the catalog valuation setup flow.

Changes

Catalog report copy

Layer / File(s) Summary
Derive viewer catalog ownership
hooks/useOwnsCatalog.ts, hooks/useCatalogReport.ts
Computes whether the viewer has catalogs and exposes that value through the catalog report hook.
Wire ownership into empty-state rendering
components/Catalog/report/CatalogReportContent.tsx, components/Catalog/report/CatalogReportEmptyState.tsx
Passes hasOwnCatalogs from the report hook into empty-state copy generation and shares the CTA styling constant.
Apply viewer-specific report guidance
lib/catalog/getCatalogReportEmptyCopy.ts
Adds the viewer contract, updates other-account body text, and uses the valuation setup CTA when the viewer has no catalogs.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Poem

Another account’s numbers gently sway,
While ownership guides the way.
With catalogs, the usual path appears;
Without them, setup now draws near.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 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.
Solid & Clean Code ✅ Passed PASS: The change stays localized—copy selection is isolated in a pure helper, hook composition remains single-purpose, and the new viewer flag is threaded with minimal duplication.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cross-account-copy-after-auth

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.

cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes Jul 30, 2026

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

No issues found across 2 files

Confidence score: 5/5

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

Auto-approved: Updates cross-account empty-state copy to remove a dead reference to a tab that now fails, and adds a guard test. The change is bounded and clearly beneficial, with implementation already reviewed.

Re-trigger cubic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Joy - How can we put an action button on the screen to help create a happy path rather than leave this as a dead end with text?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, and it was worse than "just text" — there was a button, but it was only a happy path for people who already have catalogs. Go to your catalogs sends a stranger who followed a shared link to an empty page: a second dead end, one click further in. That is the exact shape of the original bug.

Fixed in e781cd6f by making the action adapt to the viewer:

Viewer Action
has catalogs Go to your catalogs/catalogs
has none (the shared-link stranger) Value your catalog/setup/artists

/setup/artists is the step that actually creates a catalog, so the person who arrived curious about someone else's number is now one click from getting their own. It stays in-app, which keeps the architecture decision on this issue intact (the empty state never bounces anyone to recoupable.dev).

Body copy now leads into the action rather than explaining the refusal twice: "Its play counts and valuation belong to the account that measured it. Here is how to get this report for your own music."

useOwnsCatalog already loads the viewer's catalog list to decide ownership, so the flag costs no extra request.

Four tests cover it, including one asserting the cross-account state always has something to click, in either branch — so this cannot silently regress to a dead end again.

Review (@sweetmantech): the state should offer a happy path rather than a
paragraph and a dead end.

It already had a button, but "Go to your catalogs" is only a happy path for
someone who has catalogs. A stranger following a shared catalog link usually
has none, so that button landed them on an empty page — a second dead end.

The action now adapts: viewers with catalogs still go to theirs, and viewers
with none get "Value your catalog" pointing at /setup/artists, the step that
creates one. Body copy leads into the action instead of explaining the
refusal twice.

useOwnsCatalog already loads the viewer's catalog list for the ownership
check, so the flag costs no extra request.

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/Catalog/report/CatalogReportEmptyState.tsx`:
- Around line 18-24: Define and export a TypeScript interface named
CatalogReportEmptyStateProps for the state and hasOwnCatalogs props, then
replace the inline object type in CatalogReportEmptyState’s signature with that
interface.
🪄 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: c4ea6a52-4ef0-468a-bba0-6482eb5a7019

📥 Commits

Reviewing files that changed from the base of the PR and between 4a0b9ab and e781cd6.

⛔ Files ignored due to path filters (1)
  • lib/catalog/__tests__/getCatalogReportEmptyCopy.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (5)
  • components/Catalog/report/CatalogReportContent.tsx
  • components/Catalog/report/CatalogReportEmptyState.tsx
  • hooks/useCatalogReport.ts
  • hooks/useOwnsCatalog.ts
  • lib/catalog/getCatalogReportEmptyCopy.ts

Comment on lines +18 to +24
const CatalogReportEmptyState = ({
state,
hasOwnCatalogs,
}: {
state: EmptyState;
hasOwnCatalogs: boolean;
}) => {

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use an exported props interface for the component contract.

Define CatalogReportEmptyStateProps and use it in the component signature instead of an inline object type.

Suggested change
+export interface CatalogReportEmptyStateProps {
+  state: EmptyState;
+  hasOwnCatalogs: boolean;
+}
+
 const CatalogReportEmptyState = ({
   state,
   hasOwnCatalogs,
-}: {
-  state: EmptyState;
-  hasOwnCatalogs: boolean;
-}) => {
+}: CatalogReportEmptyStateProps) => {

As per coding guidelines, component prop types must use explicit exported ComponentNameProps names; as per path instructions, React component props must use TypeScript interfaces.

📝 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 CatalogReportEmptyState = ({
state,
hasOwnCatalogs,
}: {
state: EmptyState;
hasOwnCatalogs: boolean;
}) => {
export interface CatalogReportEmptyStateProps {
state: EmptyState;
hasOwnCatalogs: boolean;
}
const CatalogReportEmptyState = ({
state,
hasOwnCatalogs,
}: CatalogReportEmptyStateProps) => {
🤖 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/Catalog/report/CatalogReportEmptyState.tsx` around lines 18 - 24,
Define and export a TypeScript interface named CatalogReportEmptyStateProps for
the state and hasOwnCatalogs props, then replace the inline object type in
CatalogReportEmptyState’s signature with that interface.

Sources: Coding guidelines, Path instructions

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

2 issues found across 6 files (changes from recent commits).

Confidence score: 3/5

  • In lib/catalog/getCatalogReportEmptyCopy.ts, the “Value your catalog” CTA appears to route viewers with completed roster/social setup but no catalog back to the first onboarding screen, which can trap users in repeated setup and increase drop-off; update this branch to send them to the intended post-setup destination instead.
  • In lib/catalog/__tests__/getCatalogReportEmptyCopy.test.ts, the new “sends a viewer who has catalogs to their own” test duplicates an existing cross-account in-app test, which weakens test clarity and can hide intent-specific regressions; consolidate or rename so each test covers a distinct scenario.
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="lib/catalog/getCatalogReportEmptyCopy.ts">

<violation number="1" location="lib/catalog/getCatalogReportEmptyCopy.ts:59">
P2: Viewers who already completed roster/social setup but have no catalog are sent back to the first onboarding screen when they click “Value your catalog,” forcing them to repeat completed steps. Point this CTA at the derived `/setup` entry so the onboarding flow selects the viewer’s actual next step.</violation>
</file>

<file name="lib/catalog/__tests__/getCatalogReportEmptyCopy.test.ts">

<violation number="1" location="lib/catalog/__tests__/getCatalogReportEmptyCopy.test.ts:73">
P3: New test "sends a viewer who has catalogs to their own" duplicates the existing "keeps a cross-account viewer in-app" test — same function call, same assertion. Either rename the existing test to cover both intents or remove this duplicate.</violation>
</file>

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

Re-trigger cubic

if (state === "other-account" && !viewer.hasOwnCatalogs) {
return {
...copy,
cta: { label: "Value your catalog", href: "/setup/artists" },

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: Viewers who already completed roster/social setup but have no catalog are sent back to the first onboarding screen when they click “Value your catalog,” forcing them to repeat completed steps. Point this CTA at the derived /setup entry so the onboarding flow selects the viewer’s actual next step.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/catalog/getCatalogReportEmptyCopy.ts, line 59:

<comment>Viewers who already completed roster/social setup but have no catalog are sent back to the first onboarding screen when they click “Value your catalog,” forcing them to repeat completed steps. Point this CTA at the derived `/setup` entry so the onboarding flow selects the viewer’s actual next step.</comment>

<file context>
@@ -40,6 +45,20 @@ const COPY: Record<CatalogReportEmptyState, CatalogReportEmptyCopy> = {
+  if (state === "other-account" && !viewer.hasOwnCatalogs) {
+    return {
+      ...copy,
+      cta: { label: "Value your catalog", href: "/setup/artists" },
+    };
+  }
</file context>
Suggested change
cta: { label: "Value your catalog", href: "/setup/artists" },
cta: { label: "Value your catalog", href: "/setup" },

// way forward, not a paragraph. "Go to your catalogs" is only a happy path
// for someone who has catalogs — a stranger following a shared link usually
// has none, and would land on an empty page. So the action adapts.
it("sends a viewer who has catalogs to their own", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: New test "sends a viewer who has catalogs to their own" duplicates the existing "keeps a cross-account viewer in-app" test — same function call, same assertion. Either rename the existing test to cover both intents or remove this duplicate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/catalog/__tests__/getCatalogReportEmptyCopy.test.ts, line 73:

<comment>New test "sends a viewer who has catalogs to their own" duplicates the existing "keeps a cross-account viewer in-app" test — same function call, same assertion. Either rename the existing test to cover both intents or remove this duplicate.</comment>

<file context>
@@ -38,28 +41,56 @@ describe("getCatalogReportEmptyCopy", () => {
+  // way forward, not a paragraph. "Go to your catalogs" is only a happy path
+  // for someone who has catalogs — a stranger following a shared link usually
+  // has none, and would land on an empty page. So the action adapts.
+  it("sends a viewer who has catalogs to their own", () => {
+    const copy = getCatalogReportEmptyCopy("other-account", withCatalogs);
+
</file context>

@cubic-dev-ai
cubic-dev-ai Bot dismissed their stale review July 30, 2026 21:42

Dismissed because Cubic found issues in a newer review.

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Preview verification — 2026-07-30

Preview https://chat-7tdt85jib-recoup.vercel.app, built from head e781cd6f (resolved by sha), signed in as sweetman@recoupable.com — chosen deliberately because that account has no catalogs, which is the branch the review comment created.

The api test branch was synced with main first (it was 2 commits behind — api#800 and api#802). Chat previews resolve to test-recoup-api, so without that sync this preview would have tested against an API that still served the songs tab, and the whole point of this PR would have been unverifiable.

Results

# Check Actual
1 Non-owner opens a shared catalog URL "This catalog was measured by another account"
2 Body no longer promises the songs tab "listed under Manage songs" absent from the page
3 Adaptive CTA for a viewer with no catalogs "Value your catalog"/setup/artists
4 The CTA actually goes somewhere useful click → lands on /setup/artists, "Step 1 of 2 — Confirm your roster"
5 Manage songs tab for a non-owner renders the same honest state; no tracklist, no ISRCs, no "Found 24 songs"
6 Still no off-app CTA zero recoupable.dev in text or any href

Row 5 is the reason this PR exists. With api#802 live on test, that tab now refuses a non-owner. The old copy pointed them straight at it. Had this shipped without the reword, the page would have said "The songs are listed under Manage songs" directly above a tab that returns 403 — the same self-contradiction the issue was opened for, just relocated.

Row 4 matters more than it looks. The old CTA (Go to your catalogs) was technically a button, but for a stranger following a shared link it led to an empty page — a dead end one click deeper. Now it leads to the step that actually creates a catalog.

Screenshots

Non-owner on a shared catalog — honest state with a real next action:

cross-account with Value your catalog

Manage songs tab, same viewer — the tracklist and ISRCs are gone:

manage songs no longer leaks

The CTA lands on the roster step, not an empty page:

CTA lands on setup

Review

@sweetmantech asked for a happy path rather than a dead end. Addressed in e781cd6f and replied on the thread: the action adapts, /catalogs for viewers who have catalogs and /setup/artists for those who do not. One test asserts the cross-account state always has something to click in either branch, so it cannot regress to a dead end again.

Suite 337 passed / 82 files, tsc --noEmit and eslint clean.

Note

This is the last link of the row 6 chain (docs#282api#802 → this). Both earlier links are merged, so production currently refuses the songs tab while still telling non-owners to go there. This should merge promptly rather than sit.

@sweetmantech
sweetmantech merged commit bf50e10 into main Jul 30, 2026
4 checks passed
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