PWA manager is now visible to all users

This commit is contained in:
Dobromir Popov
2024-05-07 14:00:29 +03:00
parent 885f98e83e
commit c30928547c
6 changed files with 173 additions and 107 deletions

1
.env
View File

@ -10,6 +10,7 @@ NODE_ENV=development
# mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma # mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma
DATABASE=mysql://cart:cartpw@localhost:3306/cart DATABASE=mysql://cart:cartpw@localhost:3306/cart
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev # DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София # // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716 # // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716

View File

@ -35,9 +35,11 @@ services:
MYSQL_DATABASE: jwpwsofia_demo MYSQL_DATABASE: jwpwsofia_demo
MYSQL_USER: jwpwsofia_demo MYSQL_USER: jwpwsofia_demo
MYSQL_PASSWORD: dwxhns9p9vp248 MYSQL_PASSWORD: dwxhns9p9vp248
# networks: adminer:
# - infrastructure_default image: adminer
# - default restart: always
ports:
- 5002:8080
networks: networks:
infrastructure_default: infrastructure_default:
external: true external: true

View File

@ -3,6 +3,8 @@ import common from '../src/helpers/common'; // Ensure this path is correct
//use session to get user role //use session to get user role
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import e from 'express'; import e from 'express';
import ProtectedRoute from './protectedRoute';
import { UserRole } from '@prisma/client';
function PwaManager() { function PwaManager() {
const [deferredPrompt, setDeferredPrompt] = useState(null); const [deferredPrompt, setDeferredPrompt] = useState(null);
@ -12,8 +14,14 @@ function PwaManager() {
const [subscription, setSubscription] = useState(null); const [subscription, setSubscription] = useState(null);
const [registration, setRegistration] = useState(null); const [registration, setRegistration] = useState(null);
const [notificationPermission, setNotificationPermission] = useState(Notification.permission); const [notificationPermission, setNotificationPermission] = useState(Notification.permission);
const [subs, setSubs] = useState("")
const { data: session } = useSession(); const { data: session } = useSession();
// let isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
let isAdmin = false;
if (session) {
isAdmin = session.user.role === UserRole.ADMIN;
}
// Handle PWA installation // Handle PWA installation
useEffect(() => { useEffect(() => {
@ -33,9 +41,13 @@ function PwaManager() {
}); });
} }
// Check if the app is running in standalone mode // 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) { if (window.matchMedia('(display-mode: standalone)').matches) {
setIsStandAlone(true); setIsStandAlone(true);
} }
@ -58,19 +70,28 @@ function PwaManager() {
}; };
}, []); }, []);
const installPWA = async (e) => { const installPWA = async (e) => {
console.log('installing PWA'); console.log('Attempting to install PWA');
e.preventDefault(); e.preventDefault(); // Prevent default button action
if (deferredPrompt) { if (deferredPrompt) {
console.log('Prompting install');
deferredPrompt.prompt(); deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice; const { outcome } = await deferredPrompt.userChoice;
console.log('Installation outcome:', outcome);
if (outcome === 'accepted') { if (outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
setIsPWAInstalled(true); setIsPWAInstalled(true);
} else {
console.log('User dismissed the A2HS prompt');
} }
setDeferredPrompt(null); setDeferredPrompt(null); // Clear the deferred prompt to manage its lifecycle
} else {
console.log('No deferred prompt available');
} }
}; };
const subscribeToNotifications = async (e) => { const subscribeToNotifications = async (e) => {
try { try {
@ -90,7 +111,9 @@ function PwaManager() {
if (!vapidPublicKey) { if (!vapidPublicKey) {
// Fetch the public key from the server if not present in env variables // Fetch the public key from the server if not present in env variables
const response = await fetch('/api/notify', { method: 'GET' }); const response = await fetch('/api/notify', { method: 'GET' });
vapidPublicKey = await response.text(); const responseData = await response.json();
vapidPublicKey = responseData.pk;
setSubs(responseData.subs);
if (!vapidPublicKey) { if (!vapidPublicKey) {
throw new Error("Failed to fetch VAPID public key from server."); throw new Error("Failed to fetch VAPID public key from server.");
} }
@ -107,12 +130,14 @@ function PwaManager() {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ subscription: sub, id: session.user.id }) body: JSON.stringify({ subscription: sub, id: session.user.id })
}).then(response => { }).then(async response => {
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to save subscription data on server.'); throw new Error('Failed to save subscription data on server.');
} }
else { else {
console.log('Subscription data saved on server.'); console.log('Subscription data saved on server.');
const s = await response.json();
setSubs(s.subs);
setSubscription(sub); setSubscription(sub);
setIsSubscribed(true); setIsSubscribed(true);
console.log('Web push subscribed!'); console.log('Web push subscribed!');
@ -144,12 +169,14 @@ function PwaManager() {
//send the current subscription to be removed //send the current subscription to be removed
body: JSON.stringify({ id: session.user.id, subscriptionId: subscription.endpoint }) body: JSON.stringify({ id: session.user.id, subscriptionId: subscription.endpoint })
} }
).then(response => { ).then(async (response) => {
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to delete subscription data on server.'); throw new Error('Failed to delete subscription data on server.');
} }
else { else {
console.log('Subscription data deleted on server.'); console.log('Subscription data deleted on server.');
const s = await response.json();
setSubs(s.subs);
} }
}); });
} }
@ -261,6 +288,7 @@ function PwaManager() {
if (subscription) { if (subscription) {
await subscription.unsubscribe(); await subscription.unsubscribe();
} }
setSubs("");
setSubscription(null); setSubscription(null);
setIsSubscribed(false); setIsSubscribed(false);
} }
@ -270,77 +298,81 @@ function PwaManager() {
return ( return (
<> <>
<div> <div>
<h1>PWA Manager</h1> <h1>{isAdmin && " PWA (admin)"}</h1>
{!isStandAlone && !isPWAInstalled && ( {!isStandAlone && !isPWAInstalled && (
<button <button
onClick={installPWA} 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" 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"
> >
Install PWA Инсталирай приложението
</button> </button>
)} )}
{isPWAInstalled && <p>App is installed!</p>} {isPWAInstalled && <p>Инсталирано!</p>}
{isStandAlone && <p>PWA App</p>} {/* {isStandAlone && <p>PWA App</p>} */}
<button <button
onClick={isSubscribed ? unsubscribeFromNotifications : subscribeToNotifications} onClick={isSubscribed ? unsubscribeFromNotifications : subscribeToNotifications}
disabled={false} // Since the button itself acts as a toggle, the disabled attribute might not be needed 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'}`} > 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 ? 'Unsubscribe from Notifications' : 'Subscribe to Notifications'} {isSubscribed ? 'Спри известията' : 'Показвай известия'}
</button> </button>
<button <button
onClick={deleteAllSubscriptions} onClick={deleteAllSubscriptions}
className="text-xs py-1 px-2 rounded-full focus:outline-none bg-red-500 hover:bg-red-700 text-white" className="text-xs py-1 px-2 rounded-full focus:outline-none bg-red-500 hover:bg-red-700 text-white"
> >
Delete All Subscriptions Спри известията на всички мои устройства {subs != "" ? `(${subs})` : ""}
</button> </button>
</div > </div>
<div> {isAdmin &&
<button <div>
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'
}`}
>
Send Test Notification
</button>
<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>
{notificationPermission !== "granted" && (
<button <button
onClick={togglePushNotifications} onClick={sendTestNotification}
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' 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'
}`} }`}
> >
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'} Send Test Notification
</button> </button>
)} <button
</div> onClick={sendTestReminder}
<div> disabled={!isSubscribed}
<a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank"> 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'
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" /> }`}
<span className="align-middle">Телеграм</span> >
</a> Broadcast Reminder
</div> </button>
<div> <button
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4" target="_blank"> onClick={sendTestCoverMe}
<span className="align-middle">Apple sign-in</span> disabled={!isSubscribed}
</a> 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> </div>
}
</> </>
); );

View File

@ -250,49 +250,60 @@ export default function PublisherForm({ item, me }) {
{/* notifications */} {/* notifications */}
<div className="mb-6 p-4 border border-gray-300 rounded-lg"> <div className="mb-6 p-4 border border-gray-300 rounded-lg">
<fieldset> <fieldset>
<legend className="text-lg font-medium mb-2">Известия по имейл</legend> <legend className="text-lg font-medium mb-2">Известия</legend>
<div className="space-y-4">
<div className="form-check"> {/* Email notifications group */}
<input <div className="mb-4">
className="checkbox cursor-not-allowed opacity-50" <h3 className="text-md font-semibold mb-2">Известия по имейл</h3>
type="checkbox" <div className="space-y-4">
id="isSubscribedToCoverMeMandatory" <div className="form-check">
name="isSubscribedToCoverMeMandatory" <input
onChange={handleChange} // This will not fire due to being disabled, but kept for consistency className="checkbox cursor-not-allowed opacity-50"
checked={true} // Always checked type="checkbox"
disabled={true} // User cannot change this field id="isSubscribedToCoverMeMandatory"
autoComplete="off" /> name="isSubscribedToCoverMeMandatory"
<label className="label cursor-not-allowed opacity-50" htmlFor="isSubscribedToCoverMeMandatory"> onChange={handleChange} // This will not fire due to being disabled, but kept for consistency
Имейли за заместване които отговарят на моите предпочитания checked={true} // Always checked
</label> disabled={true} // User cannot change this field
</div> autoComplete="off" />
<div className="form-check"> <label className="label cursor-not-allowed opacity-50" htmlFor="isSubscribedToCoverMeMandatory">
<input Имейли за заместване които отговарят на моите предпочитания
className="checkbox" </label>
type="checkbox" </div>
id="isSubscribedToCoverMe" <div className="form-check">
name="isSubscribedToCoverMe" <input
onChange={handleChange} className="checkbox"
checked={publisher.isSubscribedToCoverMe} type="checkbox"
autoComplete="off" /> id="isSubscribedToCoverMe"
<label className="label" htmlFor="isSubscribedToCoverMe"> name="isSubscribedToCoverMe"
Всички заявки за заместване onChange={handleChange}
</label> checked={publisher.isSubscribedToCoverMe}
</div> autoComplete="off" />
<div className="form-check"> <label className="label" htmlFor="isSubscribedToCoverMe">
<input Всички заявки за заместване
className="checkbox" </label>
type="checkbox" </div>
id="isSubscribedToReminders" <div className="form-check">
name="isSubscribedToReminders" <input
onChange={handleChange} className="checkbox"
checked={publisher.isSubscribedToReminders} type="checkbox"
autoComplete="off" /> id="isSubscribedToReminders"
<label className="label" htmlFor="isSubscribedToReminders"> name="isSubscribedToReminders"
Други напомняния onChange={handleChange}
</label> checked={publisher.isSubscribedToReminders}
autoComplete="off" />
<label className="label" htmlFor="isSubscribedToReminders">
Други напомняния
</label>
</div>
</div> </div>
</div> </div>
{/* In-App notifications group */}
<div className="mb-4">
<h3 className="text-md font-semibold mb-2">Известия в приложението</h3>
<PwaManager />
</div>
</fieldset> </fieldset>
</div> </div>
@ -307,7 +318,6 @@ export default function PublisherForm({ item, me }) {
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className=""> <ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
<div className="border border-blue-500 border-solid p-2"> <div className="border border-blue-500 border-solid p-2">
{/* prompt to install PWA */} {/* prompt to install PWA */}
<PwaManager />
<div className="mb-4"> <div className="mb-4">
<label className="label" htmlFor="type">Тип</label> <label className="label" htmlFor="type">Тип</label>
<select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus > <select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
@ -366,7 +376,7 @@ export default function PublisherForm({ item, me }) {
{/* save */} {/* save */}
<button className="button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline" type="submit"> <button className="button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline" type="submit">
{router.query?.id ? "Update" : "Create"} {router.query?.id ? "Запази" : "Създай"}
</button> </button>
</div> </div>
</form> </form>

View File

@ -25,9 +25,20 @@ const Notification = async (req, res) => {
if (req.method == 'GET') { if (req.method == 'GET') {
res.statusCode = 200 res.statusCode = 200
res.setHeader('Allow', 'POST') res.setHeader('Allow', 'POST')
let subs = 0
if (req.query && req.query.id) {
const prisma = common.getPrismaClient();
const publisher = await prisma.publisher.findUnique({
where: { id: req.query.id },
select: { pushSubscription: true }
});
subs = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription.length : (publisher.pushSubscription ? 1 : 0);
res.end()
return
}
// send the public key in the response headers // send the public key in the response headers
//res.setHeader('Content-Type', 'text/plain') //res.setHeader('Content-Type', 'text/plain')
res.end(process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY) res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs })
res.end() res.end()
} }
if (req.method == 'PUT') { if (req.method == 'PUT') {

View File

@ -64,13 +64,23 @@ export const getServerSideProps = async (context) => {
} }
}); });
if (!item) { if (!item) {
const user = context.req.session.user; const user = context.req.session?.user;
if (!user) {
return {
// redirect to '/auth/signin'. assure it is not relative path
redirect: {
destination: process.env.NEXT_PUBLIC_PUBLIC_URL + "/auth/signin",
permanent: false,
}
}
}
const message = encodeURIComponent(`Този имейл (${user?.email}) не е регистриран. Моля свържете се с администратора.`);
return { return {
redirect: { redirect: {
destination: '/message?message=Този имейл (' + user.email + ') не е регистриран. Моля свържете се с администратора.', destination: `/message?message=${message}`,
permanent: false, permanent: false,
}, },
} };
} }
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift); // item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);