Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7d30205
feat: pass email query param to API for accountId override
Apr 6, 2026
519f15a
fix: handle array-valued query params safely
Apr 6, 2026
7cc0ddb
refactor: resolve email → accountId client-side via GET /api/accounts…
sweetmantech Apr 6, 2026
01b529f
refactor: use AccountOverrideSync instead of prop drilling email
sweetmantech Apr 6, 2026
dc1b80a
revert: remove unrelated page changes from PR
sweetmantech Apr 6, 2026
1547338
revert: remove formatting-only changes from chat.tsx
sweetmantech Apr 6, 2026
e277061
revert: remove formatting-only changes from VercelChatProvider.tsx
sweetmantech Apr 6, 2026
3e7af93
refactor: use tanstack useQuery for AccountOverrideSync
sweetmantech Apr 6, 2026
b14f607
refactor: SRP - extract fetchAccountIdByEmail to lib
sweetmantech Apr 6, 2026
c8bfa39
feat: add AccountOverrideBadge pill to layout
sweetmantech Apr 6, 2026
0aab24f
refactor: DRY - use shared tanstack query cache for AccountOverrideBadge
sweetmantech Apr 6, 2026
b3aa9bd
refactor: DRY - read from session storage set by AccountOverrideSync
sweetmantech Apr 6, 2026
50a8713
refactor: remove useEffect, derive badge visibility from query param
sweetmantech Apr 6, 2026
b7d5d4e
fix: high-contrast amber badge for account override
sweetmantech Apr 6, 2026
b934e9e
fix: brighter amber-400 badge in dark mode for visibility
sweetmantech Apr 6, 2026
bb917e1
refactor: convert to AccountOverrideProvider context
sweetmantech Apr 6, 2026
7cc4134
fix: throw on non-404 errors in fetchAccountIdByEmail
sweetmantech Apr 6, 2026
f8bac34
refactor: SRP - extract storage helpers from AccountOverrideProvider
sweetmantech Apr 6, 2026
c030a16
refactor: SRP file naming, remove useEffect with useQuery
sweetmantech Apr 6, 2026
cfb889e
refactor: move override storage libs to lib/accounts/override/
sweetmantech Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ToastContainer } from "react-toastify";
import { Toaster } from "sonner";
import { Geist, Geist_Mono } from "next/font/google";
import DeferredAnalytics from "@/components/DeferredAnalytics";
import AccountOverrideBadge from "@/components/AccountOverrideBadge";

const geist = Geist({
subsets: ["latin"],
Expand Down Expand Up @@ -71,14 +72,15 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html
lang="en"
suppressHydrationWarning
<html
lang="en"
suppressHydrationWarning
className={`${geist.variable} ${geistMono.variable}`}
>
<body className={`${geist.variable} antialiased`}>
<Suspense>
<Providers>
<AccountOverrideBadge />
<div className="flex flex-col md:flex-row">
<Sidebar />
<Header />
Expand Down
28 changes: 28 additions & 0 deletions components/AccountOverrideBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { X } from "lucide-react";
import { useAccountOverride } from "@/providers/AccountOverrideProvider";

/**
* Displays a pill badge when an account override is active.
* Reads from AccountOverrideProvider context.
*/
export default function AccountOverrideBadge() {
const { email, accountIdOverride, clear } = useAccountOverride();

if (!accountIdOverride || !email) return null;

return (
<div className="fixed top-3 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 bg-amber-500 dark:bg-amber-400 text-black px-4 py-2 rounded-full shadow-lg text-sm font-medium">
<span>Viewing as</span>
<span className="font-bold">{email}</span>
<button
onClick={clear}
className="ml-1 p-0.5 rounded-full hover:bg-black/10 transition-colors"
aria-label="Clear account override"
>
<X className="h-4 w-4" />
</button>
</div>
);
}
6 changes: 5 additions & 1 deletion hooks/useVercelChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UIMessage, FileUIPart } from "ai";
import useAvailableModels from "./useAvailableModels";
import { useLocalStorage } from "usehooks-ts";
import { DEFAULT_MODEL } from "@/lib/consts";
import { useAccountOverride } from "@/providers/AccountOverrideProvider";
import { usePaymentProvider } from "@/providers/PaymentProvider";
import useArtistFilesForMentions from "@/hooks/useArtistFilesForMentions";
import type { KnowledgeBaseEntry } from "@/lib/supabase/getArtistKnowledge";
Expand Down Expand Up @@ -163,15 +164,18 @@ export function useVercelChat({
return outputs;
}, [knowledgeFiles]);

const { accountIdOverride } = useAccountOverride();

const chatRequestBody = useMemo(
() => ({
roomId: id,
artistId,
// Only include organizationId if it's not null (schema expects string | undefined)
...(organizationId && { organizationId }),
...(accountIdOverride && { accountId: accountIdOverride }),
model,
}),
[id, artistId, organizationId, model],
[id, artistId, organizationId, accountIdOverride, model],
);

const { messages, status, stop, sendMessage, setMessages, regenerate } =
Expand Down
25 changes: 25 additions & 0 deletions lib/accounts/fetchAccountIdByEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl";

/**
* Fetches an account ID by email address via GET /api/accounts/{email}.
*
* @param email - The email address to look up
* @param accessToken - Bearer token for authentication
* @returns The account ID, or null if not found
*/
export async function fetchAccountIdByEmail(
email: string,
accessToken: string,
): Promise<string | null> {
const baseUrl = getClientApiBaseUrl();
const response = await fetch(
`${baseUrl}/api/accounts/${encodeURIComponent(email)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);

if (response.status === 404) return null;
if (!response.ok) throw new Error(`Account lookup failed: ${response.status}`);

const data = await response.json();
return data.account?.account_id ?? null;
}
9 changes: 9 additions & 0 deletions lib/accounts/override/clearStoredAccountOverride.ts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should these 3 libs be moved to

  • actual: lib/accounts/clearStoredAccountOverride.ts
  • suggested: lib/accounts/override/clearStoredAccountOverride.ts

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts";

/**
* Removes the account override from session storage.
*/
export function clearStoredAccountOverride(): void {
window.sessionStorage.removeItem(ACCOUNT_OVERRIDE_STORAGE_KEY);
window.sessionStorage.removeItem(`${ACCOUNT_OVERRIDE_STORAGE_KEY}_email`);
}
15 changes: 15 additions & 0 deletions lib/accounts/override/getStoredAccountOverride.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts";

/**
* Reads the stored account override from session storage.
*/
export function getStoredAccountOverride(): {
accountId: string | null;
email: string | null;
} {
if (typeof window === "undefined") return { accountId: null, email: null };
return {
accountId: window.sessionStorage.getItem(ACCOUNT_OVERRIDE_STORAGE_KEY),
email: window.sessionStorage.getItem(`${ACCOUNT_OVERRIDE_STORAGE_KEY}_email`),
};
}
12 changes: 12 additions & 0 deletions lib/accounts/override/setStoredAccountOverride.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts";

/**
* Persists an account override to session storage.
*/
export function setStoredAccountOverride(
accountId: string,
email: string,
): void {
window.sessionStorage.setItem(ACCOUNT_OVERRIDE_STORAGE_KEY, accountId);
window.sessionStorage.setItem(`${ACCOUNT_OVERRIDE_STORAGE_KEY}_email`, email);
}
1 change: 1 addition & 0 deletions lib/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const NEW_API_BASE_URL = IS_PROD
? "https://recoup-api.vercel.app"
: "https://test-recoup-api.vercel.app";
export const API_OVERRIDE_STORAGE_KEY = "recoup_api_override";
export const ACCOUNT_OVERRIDE_STORAGE_KEY = "recoup_account_override";
export const IN_PROCESS_PROTOCOL_ADDRESS = IS_PROD
? ("0x540C18B7f99b3b599c6FeB99964498931c211858" as Address)
: ("0x6832A997D8616707C7b68721D6E9332E77da7F6C" as Address);
Expand Down
83 changes: 83 additions & 0 deletions providers/AccountOverrideProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";

import { createContext, useContext, useState, useCallback, ReactNode } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { usePrivy } from "@privy-io/react-auth";
import { useQuery } from "@tanstack/react-query";
import { fetchAccountIdByEmail } from "@/lib/accounts/fetchAccountIdByEmail";
import { getStoredAccountOverride } from "@/lib/accounts/override/getStoredAccountOverride";
import { setStoredAccountOverride } from "@/lib/accounts/override/setStoredAccountOverride";
import { clearStoredAccountOverride } from "@/lib/accounts/override/clearStoredAccountOverride";

interface AccountOverrideContextType {
accountIdOverride: string | null;
email: string | null;
clear: () => void;
}

const AccountOverrideContext = createContext<AccountOverrideContextType>({
accountIdOverride: null,
email: null,
clear: () => {},
});

/**
* Provider that manages the account override lifecycle.
* Reads ?email= from URL, resolves to accountId, persists in session storage.
* Single source of truth for all override consumers.
* Placed inside PrivyProvider because it needs getAccessToken.
*/
export function AccountOverrideProvider({ children }: { children: ReactNode }) {
const searchParams = useSearchParams();
const router = useRouter();
const { getAccessToken } = usePrivy();
const emailParam = searchParams.get("email");

const [stored, setStored] = useState(getStoredAccountOverride);
const email = emailParam || stored.email;
const isClear = emailParam === "clear";

const { data: resolvedAccountId } = useQuery({
queryKey: ["accountOverride", email],
queryFn: async () => {
if (isClear) {
clearStoredAccountOverride();
setStored({ accountId: null, email: null });
return null;
}
const accessToken = await getAccessToken();
if (!accessToken) return null;
const accountId = await fetchAccountIdByEmail(email!, accessToken);
if (accountId && email) {
setStoredAccountOverride(accountId, email);
setStored({ accountId, email });
}
return accountId;
},
enabled: (!!email || isClear) && !stored.accountId,
staleTime: Infinity,
});

const accountIdOverride = stored.accountId || resolvedAccountId || null;

const clear = useCallback(() => {
clearStoredAccountOverride();
setStored({ accountId: null, email: null });
const params = new URLSearchParams(searchParams.toString());
params.delete("email");
const newPath = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname;
router.replace(newPath);
}, [searchParams, router]);

return (
<AccountOverrideContext.Provider value={{ accountIdOverride, email, clear }}>
{children}
</AccountOverrideContext.Provider>
);
}

export function useAccountOverride() {
return useContext(AccountOverrideContext);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SRP - create lib files so the provider has less than 100 lines of code.

27 changes: 15 additions & 12 deletions providers/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,40 @@ import { MiniAppProvider } from "./MiniAppProvider";
import { ThemeProvider } from "./ThemeProvider";
import { OrganizationProvider } from "./OrganizationProvider";
import ApiOverrideSync from "./ApiOverrideSync";
import { AccountOverrideProvider } from "./AccountOverrideProvider";

const queryClient = new QueryClient();

const Providers = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<ApiOverrideSync />
<ThemeProvider
attribute="class"
defaultTheme="system"
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem={true}
disableTransitionOnChange
>
<WagmiProvider>
<PrivyProvider>
<AccountOverrideProvider>
<MiniKitProvider>
<MiniAppProvider>
<UserProvider>
<OrganizationProvider>
<FunnelReportProvider>
<ArtistProvider>
<SidebarExpansionProvider>
<ConversationsProvider>
<PaymentProvider>{children}</PaymentProvider>
</ConversationsProvider>
</SidebarExpansionProvider>
</ArtistProvider>
</FunnelReportProvider>
<FunnelReportProvider>
<ArtistProvider>
<SidebarExpansionProvider>
<ConversationsProvider>
<PaymentProvider>{children}</PaymentProvider>
</ConversationsProvider>
</SidebarExpansionProvider>
</ArtistProvider>
</FunnelReportProvider>
</OrganizationProvider>
</UserProvider>
</MiniAppProvider>
</MiniKitProvider>
</AccountOverrideProvider>
</PrivyProvider>
</WagmiProvider>
</ThemeProvider>
Expand Down
Loading