feat(c6): notificações e push — Web Push VAPID, badge dinâmico, Share API
FR-6.1/6.2: Sandra recebe push quando pedido entra em pending_approval; Rafael recebe quando pedido é aprovado ou recusado. Service worker registrado em background (PWA-ready via public/sw.js). FR-6.3: Badge na Topbar busca GET /notifications/pending-count (supervisores veem count de pending_approval; reps veem 0). Intervalo de 30s. FR-6.4: Botão Compartilhar no OrderDetailPage para pedidos approved/invoiced (apenas reps). Usa navigator.share() com texto formatado para WhatsApp. Infra: modelo PushSubscription (Prisma), NotificationsModule (subscribe/ unsubscribe/pending-count + PushService VAPID), VAPID keys em .env, integração no OrdersService (create → supervisores, approve/reject → repId). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
42
apps/web/src/lib/hooks/usePushRegistration.ts
Normal file
42
apps/web/src/lib/hooks/usePushRegistration.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect } from 'react';
|
||||
import { apiFetch } from '../api-client';
|
||||
|
||||
const VAPID_PUBLIC_KEY = import.meta.env['VITE_VAPID_PUBLIC_KEY'] as string | undefined;
|
||||
|
||||
function urlBase64ToUint8Array(base64: string): Uint8Array {
|
||||
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
||||
const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
const raw = atob(b64);
|
||||
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
export function usePushRegistration() {
|
||||
useEffect(() => {
|
||||
if (!VAPID_PUBLIC_KEY || !('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
||||
|
||||
const register = async () => {
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const existing = await reg.pushManager.getSubscription();
|
||||
const sub =
|
||||
existing ??
|
||||
(await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
|
||||
}));
|
||||
const json = sub.toJSON();
|
||||
await apiFetch('/notifications/subscribe', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
endpoint: sub.endpoint,
|
||||
keys: { p256dh: json.keys?.['p256dh'] ?? '', auth: json.keys?.['auth'] ?? '' },
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Push é opt-in — permissão negada ou SW não disponível é normal
|
||||
}
|
||||
};
|
||||
|
||||
void register();
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user