Vitron-Front/app/components/notifications/PushPermissionBanner.tsx
Arda Samadi fef53fd003 feat(notifications): web push opt-in + unread nav badges
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>
2026-06-17 14:02:56 +03:30

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