Skip to content

Commit a356f2b

Browse files
refactor: remove artist scope from account emails
1 parent 1343fbd commit a356f2b

File tree

6 files changed

+159
-83
lines changed

6 files changed

+159
-83
lines changed

app/api/account-emails/route.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

components/Files/FileInfoDialog.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@ import { extractAccountIds } from "@/utils/extractAccountIds";
1111
import FileInfoDialogHeader from "./FileInfoDialogHeader";
1212
import FileInfoDialogContent from "./FileInfoDialogContent";
1313
import FileInfoDialogMetadata from "./FileInfoDialogMetadata";
14-
import { useQuery } from "@tanstack/react-query";
15-
import { Tables } from "@/types/database.types";
1614
import { useUserProvider } from "@/providers/UserProvder";
17-
18-
type AccountEmail = Tables<"account_emails">;
15+
import { useAccountEmails } from "@/hooks/useAccountEmails";
1916

2017
type FileInfoDialogProps = {
2118
file: FileRow | null;
@@ -38,19 +35,10 @@ export default function FileInfoDialog({ file, open, onOpenChange }: FileInfoDia
3835
const canEdit = file ? isTextFile(file.file_name) : false;
3936

4037
// Fetch owner email
41-
const { data: emails } = useQuery<AccountEmail[]>({
42-
queryKey: ["file-owner-email", ownerAccountId, artistAccountId],
43-
queryFn: async () => {
44-
if (!ownerAccountId || !artistAccountId || !userData) return [];
45-
const params = new URLSearchParams();
46-
params.append("accountIds", ownerAccountId);
47-
params.append("currentAccountId", userData.id);
48-
params.append("artistAccountId", artistAccountId);
49-
const response = await fetch(`/api/account-emails?${params}`);
50-
if (!response.ok) return [];
51-
return response.json();
52-
},
53-
enabled: !!ownerAccountId && !!artistAccountId && !!userData && open,
38+
const { data: emails } = useAccountEmails({
39+
accountIds: ownerAccountId ? [ownerAccountId] : [],
40+
enabled: open,
41+
queryKey: ["file-owner-email", ownerAccountId],
5442
});
5543

5644
const ownerEmail = emails?.[0]?.email || undefined;
@@ -121,4 +109,3 @@ export default function FileInfoDialog({ file, open, onOpenChange }: FileInfoDia
121109
</Dialog>
122110
);
123111
}
124-

components/TasksPage/TasksList.tsx

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import { Tables } from "@/types/database.types";
21
import { Task } from "@/lib/tasks/getTasks";
32
import TaskCard from "@/components/VercelChat/tools/tasks/TaskCard";
43
import TaskSkeleton from "./TaskSkeleton";
54
import TaskDetailsDialog from "@/components/VercelChat/dialogs/tasks/TaskDetailsDialog";
6-
import { useArtistProvider } from "@/providers/ArtistProvider";
75
import { useUserProvider } from "@/providers/UserProvder";
86
import { useMemo } from "react";
9-
import { useQuery } from "@tanstack/react-query";
10-
11-
type AccountEmail = Tables<"account_emails">;
7+
import { useAccountEmails } from "@/hooks/useAccountEmails";
128

139
interface TasksListProps {
1410
tasks: Task[];
@@ -18,7 +14,6 @@ interface TasksListProps {
1814

1915
const TasksList: React.FC<TasksListProps> = ({ tasks, isLoading, isError }) => {
2016
const { userData } = useUserProvider();
21-
const { selectedArtist } = useArtistProvider();
2217

2318
// Extract unique account IDs from tasks
2419
const accountIds = useMemo(
@@ -27,21 +22,9 @@ const TasksList: React.FC<TasksListProps> = ({ tasks, isLoading, isError }) => {
2722
);
2823

2924
// Batch fetch emails for all task owners
30-
const { data: accountEmails = [] } = useQuery<AccountEmail[]>({
25+
const { data: accountEmails = [] } = useAccountEmails({
26+
accountIds,
3127
queryKey: ["task-owner-emails", accountIds],
32-
queryFn: async () => {
33-
if (accountIds.length === 0 || !userData) return [];
34-
const params = new URLSearchParams();
35-
accountIds.forEach(id => params.append("accountIds", id));
36-
params.append("currentAccountId", userData.id);
37-
if (selectedArtist) {
38-
params.append("artistAccountId", selectedArtist.account_id);
39-
}
40-
const response = await fetch(`/api/account-emails?${params}`);
41-
if (!response.ok) throw new Error("Failed to fetch emails");
42-
return response.json();
43-
},
44-
enabled: accountIds.length > 0 && !!userData,
4528
});
4629

4730
// Create lookup map for O(1) email access

hooks/useAccountEmails.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
import { usePrivy } from "@privy-io/react-auth";
5+
import { fetchAccountEmails, type AccountEmail } from "@/lib/accounts/fetchAccountEmails";
6+
7+
interface UseAccountEmailsParams {
8+
accountIds: string[];
9+
enabled?: boolean;
10+
queryKey?: readonly unknown[];
11+
}
12+
13+
/**
14+
* Fetches account email rows for one or more account IDs the authenticated user can access.
15+
*/
16+
export function useAccountEmails({
17+
accountIds,
18+
enabled = true,
19+
queryKey = ["account-emails", accountIds],
20+
}: UseAccountEmailsParams) {
21+
const { getAccessToken, authenticated } = usePrivy();
22+
23+
return useQuery<AccountEmail[]>({
24+
queryKey: [...queryKey],
25+
queryFn: async () => {
26+
if (accountIds.length === 0) {
27+
return [];
28+
}
29+
30+
const accessToken = await getAccessToken();
31+
if (!accessToken) {
32+
throw new Error("Please sign in to view account emails");
33+
}
34+
35+
return fetchAccountEmails({
36+
accessToken,
37+
accountIds,
38+
});
39+
},
40+
enabled: enabled && accountIds.length > 0 && authenticated,
41+
});
42+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { fetchAccountEmails } from "../fetchAccountEmails";
3+
4+
vi.mock("@/lib/api/getClientApiBaseUrl", () => ({
5+
getClientApiBaseUrl: vi.fn(() => "https://test-recoup-api.vercel.app"),
6+
}));
7+
8+
const mockFetch = vi.fn();
9+
global.fetch = mockFetch;
10+
11+
describe("fetchAccountEmails", () => {
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
});
15+
16+
it("returns early when no account IDs are provided", async () => {
17+
const result = await fetchAccountEmails({
18+
accessToken: "token",
19+
accountIds: [],
20+
});
21+
22+
expect(result).toEqual([]);
23+
expect(mockFetch).not.toHaveBeenCalled();
24+
});
25+
26+
it("builds repeated account_id params and sends bearer auth", async () => {
27+
mockFetch.mockResolvedValue({
28+
ok: true,
29+
json: vi.fn().mockResolvedValue([
30+
{
31+
id: "email-1",
32+
account_id: "acc-1",
33+
email: "owner@example.com",
34+
updated_at: "2026-04-08T00:00:00.000Z",
35+
},
36+
]),
37+
});
38+
39+
const result = await fetchAccountEmails({
40+
accessToken: "token-123",
41+
accountIds: ["acc-1", "acc-2"],
42+
});
43+
44+
expect(mockFetch).toHaveBeenCalledWith(
45+
"https://test-recoup-api.vercel.app/api/accounts/emails?account_id=acc-1&account_id=acc-2",
46+
{
47+
method: "GET",
48+
headers: {
49+
Authorization: "Bearer token-123",
50+
},
51+
},
52+
);
53+
expect(result[0]?.email).toBe("owner@example.com");
54+
});
55+
56+
it("throws the API error message when the request fails", async () => {
57+
mockFetch.mockResolvedValue({
58+
ok: false,
59+
json: vi.fn().mockResolvedValue({ error: "Unauthorized" }),
60+
});
61+
62+
await expect(
63+
fetchAccountEmails({
64+
accessToken: "token",
65+
accountIds: ["acc-1"],
66+
}),
67+
).rejects.toThrow("Unauthorized");
68+
});
69+
});

lib/accounts/fetchAccountEmails.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl";
2+
import { Tables } from "@/types/database.types";
3+
4+
export type AccountEmail = Tables<"account_emails">;
5+
6+
interface FetchAccountEmailsParams {
7+
accessToken: string;
8+
accountIds: string[];
9+
}
10+
11+
/**
12+
* Fetches account email rows for the provided account IDs.
13+
*/
14+
export async function fetchAccountEmails({
15+
accessToken,
16+
accountIds,
17+
}: FetchAccountEmailsParams): Promise<AccountEmail[]> {
18+
if (accountIds.length === 0) {
19+
return [];
20+
}
21+
22+
const url = new URL(`${getClientApiBaseUrl()}/api/accounts/emails`);
23+
accountIds.forEach(accountId => {
24+
url.searchParams.append("account_id", accountId);
25+
});
26+
27+
const response = await fetch(url.toString(), {
28+
method: "GET",
29+
headers: {
30+
Authorization: `Bearer ${accessToken}`,
31+
},
32+
});
33+
34+
if (!response.ok) {
35+
const error = await response.json().catch(() => null);
36+
throw new Error(error?.error || "Failed to fetch account emails");
37+
}
38+
39+
return response.json();
40+
}

0 commit comments

Comments
 (0)