winupdate-neo/app/components/PushSubscription.tsx

144 lines
5.0 KiB
TypeScript

"use client";
import { useState, useEffect } from 'react';
import { Bell, BellOff, Send } from 'lucide-react';
import { urlBase64ToUint8Array } from '../utils/webPush';
export default function PushSubscription() {
const [isSubscribed, setIsSubscribed] = useState(false);
const [subscription, setSubscription] = useState<PushSubscription | null>(null);
const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window) {
// Register SW if not already (though it might be done in layout or separate file)
navigator.serviceWorker.ready.then(reg => {
setRegistration(reg);
reg.pushManager.getSubscription().then(sub => {
if (sub) {
setSubscription(sub);
setIsSubscribed(true);
}
});
});
}
}, []);
const subscribeUser = async () => {
if (!registration) return;
try {
const response = await fetch('/api/push/vapid-public-key');
const { publicKey } = await response.json();
const convertedVapidKey = urlBase64ToUint8Array(publicKey);
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(sub),
});
setSubscription(sub);
setIsSubscribed(true);
alert('订阅成功!');
} catch (error) {
console.error('Failed to subscribe the user: ', error);
alert('订阅失败,请检查控制台日志。');
}
};
const unsubscribeUser = async () => {
if (!subscription) return;
try {
await subscription.unsubscribe();
// Optionally notify backend to remove subscription
setSubscription(null);
setIsSubscribed(false);
alert('已取消订阅。');
} catch (error) {
console.error('Error unsubscribing', error);
}
};
const sendTestPush = async () => {
try {
const res = await fetch('/api/push/test', { method: 'POST' });
if (!res.ok) throw new Error('Failed to send test push');
alert('测试推送已发送,请检查通知!');
} catch (error) {
console.error('Failed to send test push', error);
alert('发送测试推送失败');
}
};
if (!registration) {
return null; // Service Worker not ready or not supported
}
if (!isSubscribed) {
return (
<button
onClick={subscribeUser}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
title="开启推送通知"
>
<BellOff className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline"></span>
</button>
);
}
return (
<div className="relative">
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 text-blue-600 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/40"
>
<Bell className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline"></span>
</button>
{isMenuOpen && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setIsMenuOpen(false)}
/>
<div className="absolute right-0 top-full w-40 pt-1 z-50">
<div className="bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 py-1">
<button
onClick={() => {
sendTestPush();
setIsMenuOpen(false);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center"
>
<Send className="h-3 w-3 mr-2" />
</button>
<button
onClick={() => {
unsubscribeUser();
setIsMenuOpen(false);
}}
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center"
>
<BellOff className="h-3 w-3 mr-2" />
</button>
</div>
</div>
</>
)}
</div>
);
}