Vitron-Front/app/hooks/useBadgeCounts.ts
fazli 0b18438a97 fix(notif+auth): camelCase badge fields, seller push prompt, only 401 signs out
- 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>
2026-07-18 13:18:08 +00:00

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 });
},
});
}