nexus/ui/src/components/NotificationPermissionPrompt.tsx
Nexus Dev 862cf7fef3 feat(26-04): create push API client, usePushNotifications hook, and NotificationPermissionPrompt
- Add ui/src/api/push.ts with getVapidPublicKey, subscribe, unsubscribe methods
- Add ui/src/hooks/usePushNotifications.ts with SW pushManager subscription flow
- urlBase64ToUint8Array utility converts VAPID key for applicationServerKey
- NotificationPermissionPrompt shows after 3rd agent response (engagement gate)
- Checks nexus.notifPromptDismissed localStorage key for dismiss state
- ChatPanel tracks agentResponseCount from assistant messages and renders prompt
- Install idb package (missing dependency from plan 26-00 prerequisites)
2026-04-04 03:55:48 +00:00

64 lines
2 KiB
TypeScript

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { usePushNotifications } from "../hooks/usePushNotifications";
const DISMISS_KEY = "nexus.notifPromptDismissed";
interface NotificationPermissionPromptProps {
agentResponseCount: number;
}
/**
* Notification permission prompt banner.
*
* Shows when:
* - Push notifications are supported in this browser
* - Permission is still "default" (not granted or denied)
* - User has not previously dismissed this prompt
* - User has received at least 3 agent responses (engagement gate)
*/
export function NotificationPermissionPrompt({ agentResponseCount }: NotificationPermissionPromptProps) {
const { isSupported, permission, subscribe } = usePushNotifications();
const [dismissed, setDismissed] = useState(() => {
try {
return localStorage.getItem(DISMISS_KEY) === "true";
} catch {
return false;
}
});
if (!isSupported) return null;
if (permission !== "default") return null;
if (dismissed) return null;
if (agentResponseCount < 3) return null;
const handleAllow = async () => {
await subscribe();
};
const handleDismiss = () => {
try {
localStorage.setItem(DISMISS_KEY, "true");
} catch {
// localStorage unavailable — dismiss for session only
}
setDismissed(true);
};
return (
<div className="fixed bottom-20 left-4 right-4 md:bottom-auto md:top-16 md:left-auto md:right-4 md:max-w-sm z-50 bg-card border border-border rounded-lg shadow-lg p-4">
<p className="text-sm font-semibold">Stay in the loop</p>
<p className="text-xs text-muted-foreground mt-1">
Get notified when your agents complete tasks or need input.
</p>
<div className="flex gap-2 mt-3">
<Button size="sm" onClick={handleAllow}>
Allow notifications
</Button>
<Button variant="ghost" size="sm" onClick={handleDismiss}>
Not now
</Button>
</div>
</div>
);
}