- app/components/MyLayout.tsx / app/components/home/HomePageTopBar.tsx / app/routes/profile._index.tsx: the BadgeCounts type was declared with snake_case fields (order_updates, seller_orders) while the API's global camelCase renderer sends orderUpdates / sellerOrders on the wire — every read went to undefined, so the "buyer activity" dot on the profile icon and the order-badge on the store dashboard were silently stuck at 0. Fixed by renaming the interface to match the wire and adding sellerChat (per-store roll-up) so the store dashboard sidebar reads badges.sellerChat[storeId] instead of the buyer-side chat count. - app/hooks/useBadgeCounts.ts: interface + EMPTY constant updated to camelCase + new sellerChat map. - app/utils/api-error-handler.ts: 403 no longer force-redirects to /logout. It used to force-log-out on ANY 403/401, so an approved-but-unbadged owner (Ehsan / Noelabel) whose store dashboard fired the Instagram-tab query got a 403 → forced logout → the store page vanished mid-load and the seller landed on the home screen convinced they'd been kicked off their own store. Only 401 signs the session out now. - app/components/store/StorePushPromptCard.tsx: new dashboard-level card that nudges the store owner/staff to enable browser push. Unlike the global buyer banner it's not dismissible-forever; "بعداً یادآوری کن" hides for 24h and it comes back until the user actually subscribes. Handles the permission="denied" path with an in-browser-settings hint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
77 lines
2.1 KiB
TypeScript
77 lines
2.1 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useAuthToken } from "./use-root-data";
|
|
import { getApiBaseUrl } from "~/utils/api-config";
|
|
|
|
// The API returns these as camelCase (DRF's global camel-case renderer converts
|
|
// the snake_case source names). Match the wire shape here so reads aren't
|
|
// silently undefined.
|
|
export interface BadgeCounts {
|
|
chat: number;
|
|
sellerChat: Record<string, number>;
|
|
orderUpdates: number;
|
|
sellerOrders: number;
|
|
}
|
|
|
|
const EMPTY: BadgeCounts = {
|
|
chat: 0,
|
|
sellerChat: {},
|
|
orderUpdates: 0,
|
|
sellerOrders: 0,
|
|
};
|
|
|
|
export const badgeKeys = {
|
|
all: ["notif-badges"] as const,
|
|
};
|
|
|
|
/**
|
|
* Polls unread counts for the nav badges (chat / buyer order updates / seller
|
|
* new orders). Lightweight: a single endpoint, refetched on an interval and
|
|
* when the tab regains focus.
|
|
*/
|
|
export function useBadgeCounts() {
|
|
const token = useAuthToken();
|
|
const apiBase = getApiBaseUrl();
|
|
|
|
return useQuery({
|
|
queryKey: badgeKeys.all,
|
|
enabled: !!token,
|
|
refetchInterval: 30000,
|
|
refetchOnWindowFocus: true,
|
|
queryFn: async (): Promise<BadgeCounts> => {
|
|
if (!token) return EMPTY;
|
|
const res = await fetch(`${apiBase}/api/notif/v1/badges/`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (!res.ok) return EMPTY;
|
|
return (await res.json()) as BadgeCounts;
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Marks a badge category ("order_updates" | "seller_orders") as read, then
|
|
* refreshes the counts. Chat clears via the existing per-thread read flow.
|
|
*/
|
|
export function useMarkBadgeRead() {
|
|
const token = useAuthToken();
|
|
const apiBase = getApiBaseUrl();
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (category: "order_updates" | "seller_orders") => {
|
|
if (!token) return;
|
|
await fetch(`${apiBase}/api/notif/v1/badges/mark-read/`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({ category }),
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: badgeKeys.all });
|
|
},
|
|
});
|
|
}
|