144 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
}
|