-
Notifications
You must be signed in to change notification settings - Fork 19
feat(onboarding): seed the catalog by kicking /api/valuation on first artist add (chat#1867) #1879
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,21 +2,31 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||
| import { usePrivy } from "@privy-io/react-auth"; | ||||||||||||||||||
| import { useQueryClient } from "@tanstack/react-query"; | ||||||||||||||||||
| import { toast } from "sonner"; | ||||||||||||||||||
| import { addSpotifyArtist } from "@/lib/artists/addSpotifyArtist"; | ||||||||||||||||||
| import { runValuation } from "@/lib/valuation/runValuation"; | ||||||||||||||||||
| import { useArtistProvider } from "@/providers/ArtistProvider"; | ||||||||||||||||||
| import { useOrganization } from "@/providers/OrganizationProvider"; | ||||||||||||||||||
| import useCatalogs from "@/hooks/useCatalogs"; | ||||||||||||||||||
| import type { SpotifyArtistSearchResult } from "@/types/spotify"; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Adds a Spotify-searched artist to the roster and refreshes + selects it. | ||||||||||||||||||
| * Wraps `addSpotifyArtist` with auth + the shared roster (ArtistProvider), | ||||||||||||||||||
| * mirroring `useAddRosterArtist`. | ||||||||||||||||||
| * | ||||||||||||||||||
| * On the **first** artist add (the account has no catalog yet) it also | ||||||||||||||||||
| * fire-and-forget kicks `POST /api/valuation` for that artist's Spotify id — | ||||||||||||||||||
| * seeding the onboarding catalog in the background so the "Claim your catalog" | ||||||||||||||||||
| * step is already complete by the time the user reaches it (chat#1867). | ||||||||||||||||||
| */ | ||||||||||||||||||
| export function useAddSpotifyArtist() { | ||||||||||||||||||
| const { getAccessToken } = usePrivy(); | ||||||||||||||||||
| const { getArtists } = useArtistProvider(); | ||||||||||||||||||
| const { selectedOrgId } = useOrganization(); | ||||||||||||||||||
| const catalogsQuery = useCatalogs(); | ||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||
| const [isAdding, setIsAdding] = useState(false); | ||||||||||||||||||
|
|
||||||||||||||||||
| const add = async (artist: SpotifyArtistSearchResult): Promise<boolean> => { | ||||||||||||||||||
|
|
@@ -33,6 +43,25 @@ export function useAddSpotifyArtist() { | |||||||||||||||||
| ); | ||||||||||||||||||
| await getArtists(created.account_id); | ||||||||||||||||||
| toast.success(`${artist.name} added to your roster`); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Seed the onboarding catalog from the first artist's discography. | ||||||||||||||||||
| // Only when we know there is no catalog yet (empty, not just unloaded), | ||||||||||||||||||
| // and fire-and-forget so the dialog closes immediately — the catalog | ||||||||||||||||||
| // materializes in ~20s and the sequence advances on the next landing. | ||||||||||||||||||
| const catalogs = catalogsQuery.data?.catalogs; | ||||||||||||||||||
| if (catalogs && catalogs.length === 0) { | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Fresh onboarding can still miss catalog seeding when the user selects an artist before Prompt for AI agents |
||||||||||||||||||
| toast.promise( | ||||||||||||||||||
| runValuation(accessToken, artist.id).then(() => { | ||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: ["catalogs"] }); | ||||||||||||||||||
|
Comment on lines
+53
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The "Your catalog is ready" success toast fires before the catalog data refresh finishes. The Prompt for AI agents
Suggested change
|
||||||||||||||||||
| }), | ||||||||||||||||||
|
Comment on lines
+53
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the target file and locate the referenced code.
git ls-files hooks/useAddSpotifyArtist.ts
wc -l hooks/useAddSpotifyArtist.ts
cat -n hooks/useAddSpotifyArtist.ts | sed -n '1,220p'
# Search for related patterns in the repository for context.
rg -n "toast\.promise|invalidateQueries\(\{ queryKey: \[\"catalogs\"\] \}\)" hooks . || trueRepository: recoupable/chat Length of output: 4193 Return the invalidation promise from Proposed fix- runValuation(accessToken, artist.id).then(() => {
- queryClient.invalidateQueries({ queryKey: ["catalogs"] });
- }),
+ runValuation(accessToken, artist.id).then(() =>
+ queryClient.invalidateQueries({ queryKey: ["catalogs"] }),
+ ),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| { | ||||||||||||||||||
| loading: `Valuing ${artist.name}'s catalog…`, | ||||||||||||||||||
| success: "Your catalog is ready", | ||||||||||||||||||
| error: | ||||||||||||||||||
| "Couldn't value the catalog automatically — you can claim it later", | ||||||||||||||||||
| }, | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| return true; | ||||||||||||||||||
| } catch (error) { | ||||||||||||||||||
| toast.error( | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { runValuation } from "@/lib/valuation/runValuation"; | ||
| import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; | ||
|
|
||
| vi.mock("@/lib/api/getClientApiBaseUrl", () => ({ | ||
| getClientApiBaseUrl: vi.fn(), | ||
| })); | ||
|
|
||
| describe("runValuation", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| vi.mocked(getClientApiBaseUrl).mockReturnValue("https://api.example.com"); | ||
| }); | ||
|
|
||
| it("POSTs the spotify_artist_id to /api/valuation with bearer auth", async () => { | ||
| global.fetch = vi | ||
| .fn() | ||
| .mockResolvedValue({ ok: true }) as unknown as typeof fetch; | ||
|
|
||
| await runValuation("tok", "0xPoV"); | ||
|
|
||
| expect(global.fetch).toHaveBeenCalledWith( | ||
| "https://api.example.com/api/valuation", | ||
| expect.objectContaining({ | ||
| method: "POST", | ||
| headers: expect.objectContaining({ Authorization: "Bearer tok" }), | ||
| body: JSON.stringify({ spotify_artist_id: "0xPoV" }), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("throws on a non-ok response", async () => { | ||
| global.fetch = vi.fn().mockResolvedValue({ | ||
| ok: false, | ||
| status: 402, | ||
| text: vi.fn().mockResolvedValue("insufficient credits"), | ||
| }) as unknown as typeof fetch; | ||
|
|
||
| await expect(runValuation("tok", "0xPoV")).rejects.toThrow("402"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; | ||
|
|
||
| /** | ||
| * Kicks `POST /api/valuation` for a Spotify artist (api#776): resolves the | ||
| * artist's releases, measures play counts, and materializes an account-owned | ||
| * catalog + value band. Used fire-and-forget on the first artist add to seed | ||
| * the onboarding catalog so "Claim your catalog" is already complete. | ||
| * | ||
| * @param accessToken - Privy bearer for the owning account. | ||
| * @param spotifyArtistId - The Spotify artist id to value. | ||
| */ | ||
| export async function runValuation( | ||
| accessToken: string, | ||
| spotifyArtistId: string, | ||
| ): Promise<void> { | ||
| const res = await fetch(`${getClientApiBaseUrl()}/api/valuation`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: JSON.stringify({ spotify_artist_id: spotifyArtistId }), | ||
| }); | ||
|
Comment on lines
+16
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '== file outline ==\n'
ast-grep outline lib/valuation/runValuation.ts --view expanded || true
printf '\n== file contents ==\n'
cat -n lib/valuation/runValuation.ts
printf '\n== references ==\n'
rg -n "runValuation\\(|valuation.*toast|toast.*valuation|AbortController|signal:" lib -SRepository: recoupable/chat Length of output: 1797 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n "runValuation\\(" .
rg -n "AbortController|signal:|timeout|setTimeout\\(" lib src app .Repository: recoupable/chat Length of output: 4364 🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline hooks/useAddSpotifyArtist.ts --view expanded || true
cat -n hooks/useAddSpotifyArtist.ts | sed -n '1,140p'Repository: recoupable/chat Length of output: 3677 Bound the valuation request. 🤖 Prompt for AI Agents |
||
|
|
||
| if (!res.ok) { | ||
| const detail = await res.text().catch(() => ""); | ||
| throw new Error(`Valuation failed: HTTP ${res.status} ${detail}`.trim()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Two rapid sequential artist adds can trigger concurrent valuations before the first one completes. Since
isAddingresets to false right after the artist-add API returns (valuation runs fire-and-forget in the background), the UI allows adding another artist while the first valuation is still in flight. At that point the catalogs query hasn't been invalidated yet, so it still reports an empty catalog — causing a secondPOST /api/valuationto fire. Consider tracking a separateisValuatingref or skipping the valuation if one is already pending to avoid duplicate catalog creation.Prompt for AI agents