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
DATABASE=mysql://cart:cartpw@localhost:3306/cart
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
# // 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_USER: jwpwsofia_demo
MYSQL_PASSWORD: dwxhns9p9vp248
# networks:
# - infrastructure_default
# - default
adminer:
image: adminer
restart: always
ports:
- 5002:8080
networks:
infrastructure_default:
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
import { useSession } from "next-auth/react"
import e from 'express';
import ProtectedRoute from './protectedRoute';
import { UserRole } from '@prisma/client';
function PwaManager() {
const [deferredPrompt, setDeferredPrompt] = useState(null);
@ -12,8 +14,14 @@ function PwaManager() {
const [subscription, setSubscription] = useState(null);
const [registration, setRegistration] = useState(null);
const [notificationPermission, setNotificationPermission] = useState(Notification.permission);
const [subs, setSubs] = useState("")
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(() => {
@ -33,9 +41,13 @@ function PwaManager() {
});
}
// 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);
}
@ -58,19 +70,28 @@ function PwaManager() {
};
}, []);
const installPWA = async (e) => {
console.log('installing PWA');
e.preventDefault();
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);
setDeferredPrompt(null); // Clear the deferred prompt to manage its lifecycle
} else {
console.log('No deferred prompt available');
}
};
const subscribeToNotifications = async (e) => {
try {
@ -90,7 +111,9 @@ function PwaManager() {
if (!vapidPublicKey) {
// Fetch the public key from the server if not present in env variables
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) {
throw new Error("Failed to fetch VAPID public key from server.");
}
@ -107,12 +130,14 @@ function PwaManager() {
'Content-Type': 'application/json'
},
body: JSON.stringify({ subscription: sub, id: session.user.id })
}).then(response => {
}).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!');
@ -144,12 +169,14 @@ function PwaManager() {
//send the current subscription to be removed
body: JSON.stringify({ id: session.user.id, subscriptionId: subscription.endpoint })
}
).then(response => {
).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);
}
});
}
@ -261,6 +288,7 @@ function PwaManager() {
if (subscription) {
await subscription.unsubscribe();
}
setSubs("");
setSubscription(null);
setIsSubscribed(false);
}
@ -270,77 +298,81 @@ function PwaManager() {
return (
<>
<div>
<h1>PWA Manager</h1>
<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"
>
Install PWA
Инсталирай приложението
</button>
)}
{isPWAInstalled && <p>App is installed!</p>}
{isStandAlone && <p>PWA App</p>}
{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 ? 'Unsubscribe from Notifications' : 'Subscribe to Notifications'}
{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"
>
Delete All Subscriptions
Спри известията на всички мои устройства {subs != "" ? `(${subs})` : ""}
</button>
</div >
<div>
<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'
}`}
>
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" && (
</div>
{isAdmin &&
<div>
<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'
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'
}`}
>
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'}
Send Test Notification
</button>
)}
</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>
</div>
<div>
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4" target="_blank">
<span className="align-middle">Apple sign-in</span>
</a>
<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>
}
</>
);

View File

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

View File

@ -25,9 +25,20 @@ const Notification = async (req, res) => {
if (req.method == 'GET') {
res.statusCode = 200
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
//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()
}
if (req.method == 'PUT') {

View File

@ -64,13 +64,23 @@ export const getServerSideProps = async (context) => {
}
});
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 {
redirect: {
destination: '/message?message=Този имейл (' + user.email + ') не е регистриран. Моля свържете се с администратора.',
destination: `/message?message=${message}`,
permanent: false,
},
}
};
}
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);