416 lines
17 KiB
TypeScript
416 lines
17 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import common from '../src/helpers/common'; // Ensure this path is correct
|
||
//use session to get user role
|
||
import { useSession } from "next-auth/react"
|
||
import e from 'express';
|
||
import ProtectedRoute from './protectedRoute';
|
||
import { UserRole } from '@prisma/client';
|
||
|
||
function PwaManager({ subs }) {
|
||
//ToDo: for iOS, try to use apn? https://github.com/node-apn/node-apn/blob/master/doc/apn.markdown
|
||
const isSupported = () =>
|
||
'Notification' in window &&
|
||
'serviceWorker' in navigator &&
|
||
'PushManager' in window
|
||
|
||
const [inProgress, setInProgress] = useState(false)
|
||
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
||
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
|
||
const [isStandAlone, setIsStandAlone] = useState(false);
|
||
const [isSubscribed, setIsSubscribed] = useState(false);
|
||
const [subscription, setSubscription] = useState(null);
|
||
const [registration, setRegistration] = useState(null);
|
||
const [notificationPermission, setNotificationPermission] = useState(isSupported() && Notification.permission);
|
||
const [_subs, setSubs] = useState(subs)
|
||
|
||
const { data: session } = useSession();
|
||
// let isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
|
||
let isAdmin = false;
|
||
if (session) {
|
||
isAdmin = session.user.role === UserRole.ADMIN;
|
||
}
|
||
|
||
|
||
// Handle PWA installation
|
||
useEffect(() => {
|
||
if (isSupported()) {
|
||
setNotificationPermission(Notification.permission);
|
||
}
|
||
|
||
// Handle Push Notification Subscription
|
||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||
navigator.serviceWorker.ready.then(swreg => {
|
||
swreg.pushManager.getSubscription().then(sub => {
|
||
if (sub) {
|
||
setSubscription(sub);
|
||
setIsSubscribed(true);
|
||
}
|
||
});
|
||
setRegistration(swreg);
|
||
});
|
||
}
|
||
|
||
// Check if the app is running in standalone mode
|
||
// const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
||
// if (isStandalone) {
|
||
// console.log('Running in standalone mode');
|
||
// setIsPWAInstalled(true);
|
||
// }
|
||
|
||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||
setIsStandAlone(true);
|
||
}
|
||
|
||
const handleBeforeInstallPrompt = (e) => {
|
||
e.preventDefault();
|
||
setDeferredPrompt(e);
|
||
};
|
||
|
||
const handleAppInstalled = () => {
|
||
setIsPWAInstalled(true);
|
||
};
|
||
|
||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||
window.addEventListener('appinstalled', handleAppInstalled);
|
||
|
||
return () => {
|
||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||
window.removeEventListener('appinstalled', handleAppInstalled);
|
||
};
|
||
}, []);
|
||
|
||
|
||
const installPWA = async (e) => {
|
||
console.log('Attempting to install PWA');
|
||
e.preventDefault(); // Prevent default button action
|
||
if (deferredPrompt) {
|
||
console.log('Prompting install');
|
||
deferredPrompt.prompt();
|
||
const { outcome } = await deferredPrompt.userChoice;
|
||
console.log('Installation outcome:', outcome);
|
||
if (outcome === 'accepted') {
|
||
console.log('User accepted the A2HS prompt');
|
||
setIsPWAInstalled(true);
|
||
} else {
|
||
console.log('User dismissed the A2HS prompt');
|
||
}
|
||
setDeferredPrompt(null); // Clear the deferred prompt to manage its lifecycle
|
||
} else {
|
||
console.log('No deferred prompt available');
|
||
}
|
||
};
|
||
|
||
|
||
const subscribeToNotifications = async (e) => {
|
||
|
||
try {
|
||
e.preventDefault();
|
||
if (!navigator.serviceWorker) {
|
||
console.error('Service worker is not supported by this browser.');
|
||
return;
|
||
}
|
||
|
||
const registration = await navigator.serviceWorker.ready;
|
||
if (!registration) {
|
||
console.error('Service worker registration not found.');
|
||
return;
|
||
}
|
||
|
||
let vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
|
||
if (!vapidPublicKey) {
|
||
// Fetch the public key from the server if not present in env variables
|
||
const response = await fetch('/api/notify', { method: 'GET' });
|
||
const responseData = await response.json();
|
||
vapidPublicKey = responseData.pk;
|
||
setSubs(responseData.subs);
|
||
if (!vapidPublicKey) {
|
||
throw new Error("Failed to fetch VAPID public key from server.");
|
||
}
|
||
}
|
||
const sub = await registration.pushManager.subscribe({
|
||
userVisibleOnly: true,
|
||
applicationServerKey: common.base64ToUint8Array(vapidPublicKey)
|
||
});
|
||
// Call your API to save subscription data on server
|
||
if (session.user?.id != null) {
|
||
await fetch(`/api/notify`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ subscription: sub, id: session.user.id })
|
||
}).then(async response => {
|
||
if (!response.ok) {
|
||
throw new Error('Failed to save subscription data on server.');
|
||
}
|
||
else {
|
||
console.log('Subscription data saved on server.');
|
||
const s = await response.json();
|
||
setSubs(s.subs);
|
||
setSubscription(sub);
|
||
setIsSubscribed(true);
|
||
console.log('Web push subscribed!');
|
||
}
|
||
});
|
||
}
|
||
console.log(sub);
|
||
} catch (error) {
|
||
console.error('Error subscribing to notifications:', error);
|
||
}
|
||
};
|
||
|
||
const unsubscribeFromNotifications = async (e) => {
|
||
|
||
try {
|
||
e.preventDefault();
|
||
if (subscription) {
|
||
await subscription.unsubscribe();
|
||
// Call your API to delete or invalidate subscription data on server
|
||
setSubscription(null);
|
||
setIsSubscribed(false);
|
||
if (session?.user?.id != null) {
|
||
await fetch(`/api/notify`,
|
||
{
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
//send the current subscription to be removed
|
||
body: JSON.stringify({ id: session.user.id, subscriptionId: subscription.endpoint })
|
||
}
|
||
).then(async (response) => {
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete subscription data on server.');
|
||
}
|
||
else {
|
||
console.log('Subscription data deleted on server.');
|
||
const s = await response.json();
|
||
setSubs(s.subs);
|
||
}
|
||
});
|
||
}
|
||
console.log('Web push unsubscribed!');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error unsubscribing from notifications:', error);
|
||
}
|
||
};
|
||
|
||
// Function to request push notification permission
|
||
const requestNotificationPermission = async (e) => {
|
||
e.preventDefault();
|
||
if (isSupported()) {
|
||
const permission = await Notification.requestPermission();
|
||
setNotificationPermission(permission);
|
||
if (permission === "granted") {
|
||
// User granted permission
|
||
subscribeToNotifications(null); // Pass the required argument here
|
||
} else {
|
||
// User denied or dismissed permission
|
||
console.log("Push notifications permission denied.");
|
||
}
|
||
}
|
||
else {
|
||
console.error('Web push not supported');
|
||
}
|
||
};
|
||
|
||
// Function to toggle push notifications
|
||
const togglePushNotifications = async (e) => {
|
||
|
||
e.preventDefault();
|
||
if (notificationPermission === "granted") {
|
||
// If already subscribed, unsubscribe
|
||
unsubscribeFromNotifications(null); // Pass null as the argument
|
||
} else if (notificationPermission === "default" || notificationPermission === "denied") {
|
||
// Request permission if not already granted
|
||
await requestNotificationPermission(e);
|
||
}
|
||
};
|
||
|
||
|
||
const sendTestNotification = async (e) => {
|
||
|
||
e.preventDefault();
|
||
if (!subscription) {
|
||
console.error('Web push not subscribed');
|
||
return;
|
||
}
|
||
|
||
await fetch('/api/notify', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
//sends test notification to the current subscription
|
||
// body: JSON.stringify({ subscription })
|
||
//sends test notification to all subscriptions of this user
|
||
body: JSON.stringify({ id: session.user.id, title: "Тестово уведомление", message: "Това е тестово уведомление" })
|
||
});
|
||
};
|
||
|
||
async function sendTestReminder(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||
event.preventDefault();
|
||
if (!subscription) {
|
||
console.error('Web push not subscribed');
|
||
return;
|
||
}
|
||
|
||
await fetch('/api/notify', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ broadcast: true, message: "Мили братя, искаме да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'" })
|
||
});
|
||
}
|
||
|
||
async function sendTestCoverMe(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||
event.preventDefault();
|
||
if (!subscription) {
|
||
console.error('Web push not subscribed');
|
||
return;
|
||
}
|
||
|
||
await fetch('/api/notify', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
broadcast: true, message: "Брат ТЕСТ търси заместник за 24-ти май от 10:00 ч. Можеш ли да го покриеш?",
|
||
//use fontawesome icons for actions
|
||
actions: [{ action: 'covermeaccepted', title: 'Да ', icon: '✅' }]
|
||
})
|
||
});
|
||
}
|
||
|
||
async function deleteAllSubscriptions(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
|
||
event.preventDefault();
|
||
await fetch(`/api/notify`,
|
||
{
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
//send the current subscription to be removed
|
||
body: JSON.stringify({ id: session.user.id })
|
||
}
|
||
).then(async response => {
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete subscription data on server.');
|
||
}
|
||
else {
|
||
console.log('ALL subscriptions data deleted on server.');
|
||
if (subscription) {
|
||
await subscription.unsubscribe();
|
||
}
|
||
setSubs("");
|
||
setSubscription(null);
|
||
setIsSubscribed(false);
|
||
}
|
||
});
|
||
}
|
||
if (!isSupported()) {
|
||
return (
|
||
<div>
|
||
<p>Това устройство не поддържа нотификации</p>
|
||
</div>
|
||
);
|
||
}
|
||
else {
|
||
return (
|
||
<>
|
||
<div>
|
||
<h1>{isAdmin && " PWA (admin)"}</h1>
|
||
{!isStandAlone && !isPWAInstalled && (
|
||
<button
|
||
onClick={installPWA}
|
||
className="bg-blue-500 hover:bg-blue-700 text-white text-xs py-1 px-2 rounded-full focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"
|
||
>
|
||
Инсталирай приложението
|
||
</button>
|
||
)}
|
||
{isPWAInstalled && <p>Инсталирано!</p>}
|
||
{/* {isStandAlone && <p>PWA App</p>} */}
|
||
|
||
<button
|
||
onClick={isSubscribed ? unsubscribeFromNotifications : subscribeToNotifications}
|
||
disabled={false} // Since the button itself acts as a toggle, the disabled attribute might not be needed
|
||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${isSubscribed ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white'}`} >
|
||
{isSubscribed ? 'Спри известията' : 'Показвай известия'}
|
||
</button>
|
||
<button
|
||
onClick={deleteAllSubscriptions}
|
||
className="text-xs py-1 px-2 rounded-full focus:outline-none bg-red-500 hover:bg-red-700 text-white"
|
||
>
|
||
Спри известията на всички мои устройства {_subs != "" ? `(${_subs})` : ""}
|
||
</button>
|
||
<button
|
||
onClick={sendTestNotification}
|
||
disabled={!isSubscribed}
|
||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||
}`}
|
||
>
|
||
Тестово уведомление
|
||
</button>
|
||
</div>
|
||
{isAdmin &&
|
||
<div>
|
||
<button
|
||
onClick={sendTestReminder}
|
||
disabled={!isSubscribed}
|
||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||
}`}
|
||
>
|
||
Broadcast Reminder
|
||
</button>
|
||
<button
|
||
onClick={sendTestCoverMe}
|
||
disabled={!isSubscribed}
|
||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||
}`}
|
||
>
|
||
Broadcast CoverMe
|
||
</button>
|
||
</div>
|
||
}
|
||
{notificationPermission !== "granted" && (
|
||
<button
|
||
onClick={togglePushNotifications}
|
||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${notificationPermission === "denied" ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white'
|
||
}`}
|
||
>
|
||
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'}
|
||
</button>
|
||
)}
|
||
|
||
{isAdmin && <div>
|
||
<div>
|
||
<a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
|
||
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
|
||
<span className="align-middle">Телеграм</span>
|
||
</a>
|
||
|
||
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4 bg-gray-100 button" target="_blank">
|
||
<span className="align-middle">Apple sign-in</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
}
|
||
</>
|
||
);
|
||
}
|
||
}
|
||
export default PwaManager;
|
||
|
||
//get server side props - subs count
|
||
export const getServerSideProps = async (context) => {
|
||
//ToDo: get the number of subscriptions from the database
|
||
return {
|
||
props: {
|
||
subs: 0
|
||
}
|
||
}
|
||
}
|