- 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>
100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
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>
|
||
);
|
||
}
|