Surfaces three events without a full notification center: - service worker (public/sw.js) gains push + notificationclick handlers (RTL, focuses an existing tab or opens the target URL). - usePushNotifications hook: permission + VAPID subscribe/unsubscribe against the backend push endpoints. - opt-in UI both ways: a one-time dismissible post-login banner (PushPermissionBanner, rendered in MyLayout) and a permanent toggle (PushSettingToggle) on buyer profile settings and seller store settings. - useBadgeCounts polls /api/notif/v1/badges/; small red badges on the bottom-nav profile icon (chat + order updates), the profile sidebar (پیامها / سفارشها) and the seller dashboard sidebar. Visiting orders clears its badge. Degrades gracefully: if the backend endpoints are absent the counts fall back to zero and nothing throws. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { Bell, X } from "lucide-react";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
import { usePushNotifications } from "~/hooks/usePushNotifications";
|
||
|
||
const DISMISS_KEY = "push_banner_dismissed";
|
||
|
||
/**
|
||
* One-time, dismissible bar prompting logged-in users to enable web push.
|
||
* Shown only when: logged in, push is supported, permission not yet decided,
|
||
* and the user hasn't dismissed or already subscribed. A permanent toggle in
|
||
* settings lets them change their mind later.
|
||
*/
|
||
export function PushPermissionBanner() {
|
||
const { user } = useRootData();
|
||
const { supported, permission, isSubscribed, subscribe, busy } =
|
||
usePushNotifications();
|
||
const [visible, setVisible] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (typeof window === "undefined") return;
|
||
const dismissed = localStorage.getItem(DISMISS_KEY) === "1";
|
||
setVisible(
|
||
!!user?.access_token &&
|
||
supported &&
|
||
permission === "default" &&
|
||
!isSubscribed &&
|
||
!dismissed
|
||
);
|
||
}, [user?.access_token, supported, permission, isSubscribed]);
|
||
|
||
if (!visible) return null;
|
||
|
||
const dismiss = () => {
|
||
localStorage.setItem(DISMISS_KEY, "1");
|
||
setVisible(false);
|
||
};
|
||
|
||
const enable = async () => {
|
||
const ok = await subscribe();
|
||
// Whether granted or denied, don't nag again from the banner.
|
||
localStorage.setItem(DISMISS_KEY, "1");
|
||
setVisible(false);
|
||
return ok;
|
||
};
|
||
|
||
return (
|
||
<div className="fixed bottom-[calc(58px+env(safe-area-inset-bottom))] lg:bottom-4 left-1/2 -translate-x-1/2 z-40 w-[92%] max-w-md lg:max-w-sm">
|
||
<div className="flex items-center gap-3 rounded-2xl bg-BLACK text-white shadow-lg px-4 py-3">
|
||
<Bell size={20} className="shrink-0" />
|
||
<p className="text-[13px] leading-5 flex-1">
|
||
اعلانها را فعال کنید تا از پیامها و وضعیت سفارشها باخبر شوید.
|
||
</p>
|
||
<button
|
||
onClick={enable}
|
||
disabled={busy}
|
||
className="shrink-0 rounded-xl bg-VITROWN_BLUE px-3 py-1.5 text-[13px] font-bold disabled:opacity-60"
|
||
>
|
||
{busy ? "..." : "فعالسازی"}
|
||
</button>
|
||
<button onClick={dismiss} aria-label="بستن" className="shrink-0 opacity-70">
|
||
<X size={18} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|