Vitron-Front/app/components/store/StorePushPromptCard.tsx
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

100 lines
4.1 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from "react";
import { Bell, BellOff, ChevronLeft } from "lucide-react";
import { usePushNotifications } from "~/hooks/usePushNotifications";
const SNOOZE_KEY = "store_push_prompt_snoozed_until";
const SNOOZE_MS = 24 * 60 * 60 * 1000; // 24h — a soft nudge, not a dismissal.
/**
* Seller-facing card at the top of the store dashboard that nudges the owner
* (or a staff member) to turn on browser/PWA notifications. Order notifications
* are business-critical, so unlike the global buyer banner this one is NOT
* dismissible-forever — a snooze hides it for a day; the prompt returns until
* push is actually subscribed. When permission was denied, we can't
* re-request programmatically, so the copy switches to a "how to re-enable in
* the browser" instruction.
*/
export function StorePushPromptCard() {
const { supported, permission, isSubscribed, subscribe, busy } =
usePushNotifications();
const [snoozed, setSnoozed] = useState(false);
useEffect(() => {
if (typeof window === "undefined") return;
const until = Number(localStorage.getItem(SNOOZE_KEY) || 0);
setSnoozed(!!until && Date.now() < until);
}, []);
// Nothing to show if the browser can't do push, they already subscribed,
// or they hit "later" in the last 24h.
if (!supported) return null;
if (isSubscribed) return null;
if (snoozed) return null;
const snoozeUntilTomorrow = () => {
localStorage.setItem(SNOOZE_KEY, String(Date.now() + SNOOZE_MS));
setSnoozed(true);
};
const enable = async () => {
await subscribe();
};
// Permission denied — we cannot prompt again from JS. Show recovery hint.
if (permission === "denied") {
return (
<div className="mb-4 rounded-2xl border border-RED/30 bg-RED/5 p-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-RED/10 grid place-items-center shrink-0">
<BellOff size={20} className="text-RED" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-[14.5px] text-BLACK">
اعلانها در مرورگر مسدود شدهاند
</p>
<p className="text-[13px] text-BLACK2 leading-6 mt-1">
برای دریافت اعلان سفارشهای جدید، از تنظیمات مرورگر یا PWA اعلانها را برای این سایت فعال کنید و سپس صفحه را رفرش کنید.
</p>
</div>
</div>
</div>
);
}
// Default (never asked) OR granted-but-not-subscribed — both resolvable by
// clicking Enable, which triggers the PushManager subscribe flow.
return (
<div className="mb-4 rounded-2xl border border-VITROWN_BLUE/20 bg-VITROWN_BLUE/5 p-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-VITROWN_BLUE/10 grid place-items-center shrink-0">
<Bell size={20} className="text-VITROWN_BLUE" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-[14.5px] text-BLACK">
اعلان سفارشهای جدید را فعال کنید
</p>
<p className="text-[13px] text-BLACK2 leading-6 mt-1">
تا لحظهای که یک مشتری سفارش میثبت، روی مرورگر یا موبایل خود پیامی میگیرید و هیچ سفارشی را از دست نمیدهید.
</p>
<div className="flex items-center gap-2 mt-3">
<button
onClick={enable}
disabled={busy}
className="inline-flex items-center gap-1.5 rounded-xl bg-VITROWN_BLUE text-white text-[13px] font-bold px-3.5 py-2 disabled:opacity-60"
>
{busy ? "..." : "فعال‌سازی"}
<ChevronLeft size={16} />
</button>
<button
onClick={snoozeUntilTomorrow}
className="text-[13px] font-semibold text-GRAY px-2 py-2"
>
بعداً یادآوری کن
</button>
</div>
</div>
</div>
</div>
);
}