mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 08:34:20 +02:00
Add allowing push notifications
This commit is contained in:
parent
db6d296f05
commit
c7941aefc3
@ -57,6 +57,5 @@ module.exports = {
|
||||
__DEV__: true,
|
||||
__LOCAL__: false,
|
||||
__TEST__: false,
|
||||
__SENDGRID_API_KEY__: false,
|
||||
},
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ module.exports = {
|
||||
__SERVER__: false,
|
||||
__LOCAL__: false,
|
||||
__TEST__: true,
|
||||
__SENDGRID_API_KEY__: "",
|
||||
localStorage: { getItem: () => null, setItem: () => null, removeItem: () => null },
|
||||
},
|
||||
maxConcurrency: 50,
|
||||
|
@ -75,8 +75,11 @@
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"@sendgrid/mail": "^7.6.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/detect-node": "^2.0.0",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/lodash": "^4.14.177",
|
||||
"@types/serviceworker": "^0.0.32",
|
||||
"@types/web-push": "^3.3.2",
|
||||
"autoprefixer": "^10.2.6",
|
||||
"axios": "^0.21.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
@ -86,6 +89,7 @@
|
||||
"cookie-parser": "^1.4.6",
|
||||
"core-js": "^3.15.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"detect-node": "^2.1.0",
|
||||
"express": "^4.17.1",
|
||||
"fs": "^0.0.1-security",
|
||||
"google-auth-library": "^7.10.1",
|
||||
@ -114,7 +118,8 @@
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"serialize-javascript": "^6.0.0",
|
||||
"serve-favicon": "^2.5.0"
|
||||
"serve-favicon": "^2.5.0",
|
||||
"web-push": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.6",
|
||||
|
@ -1,20 +1,30 @@
|
||||
import _ from "lodash"
|
||||
import React, { memo, useCallback, useState } from "react"
|
||||
import { AppDispatch } from "../../store"
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from "react"
|
||||
import isNode from "detect-node"
|
||||
import { shallowEqual, useSelector } from "react-redux"
|
||||
import { AppDispatch, AppState } from "../../store"
|
||||
import { fetchVolunteerNotifsSet } from "../../store/volunteerNotifsSet"
|
||||
import { VolunteerNotifs } from "../../services/volunteers"
|
||||
import styles from "./styles.module.scss"
|
||||
import { logoutUser } from "../../store/auth"
|
||||
import { unsetJWT } from "../../services/auth"
|
||||
import { VolunteerNotifs } from "../../services/volunteers"
|
||||
|
||||
interface Props {
|
||||
dispatch: AppDispatch
|
||||
jwt: string
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
volunteerNotifs?: VolunteerNotifs
|
||||
}
|
||||
let prevNotifs: VolunteerNotifs | undefined
|
||||
|
||||
const Notifications = ({ dispatch, jwt }: Props): JSX.Element | null => {
|
||||
const volunteerNotifs = useSelector((state: AppState) => {
|
||||
const notifs = state.volunteerNotifsSet.entity
|
||||
if (notifs) {
|
||||
prevNotifs = notifs
|
||||
return notifs
|
||||
}
|
||||
return prevNotifs
|
||||
}, shallowEqual)
|
||||
|
||||
const Notifications = ({ dispatch, jwt, volunteerNotifs }: Props): JSX.Element => {
|
||||
const hidden = volunteerNotifs?.hiddenNotifs || []
|
||||
const notifs: JSX.Element[] = []
|
||||
|
||||
@ -78,41 +88,46 @@ const Notifications = ({ dispatch, jwt, volunteerNotifs }: Props): JSX.Element =
|
||||
<form onSubmit={onSubmit2}>
|
||||
Si les conditions sanitaires te le permettent, souhaites-tu être
|
||||
bénévole à PeL 2022 ?<br />
|
||||
<input
|
||||
type="radio"
|
||||
value="inconnu"
|
||||
name="gender"
|
||||
checked={participation === "inconnu"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
-<br />
|
||||
<input
|
||||
type="radio"
|
||||
value="oui"
|
||||
name="gender"
|
||||
checked={participation === "oui"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
Oui
|
||||
<br />
|
||||
<input
|
||||
type="radio"
|
||||
value="non"
|
||||
name="gender"
|
||||
checked={participation === "non"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
Non
|
||||
<br />
|
||||
<input
|
||||
type="radio"
|
||||
value="peut-etre"
|
||||
name="gender"
|
||||
checked={participation === "peut-etre"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
Je ne sais pas encore
|
||||
<br />
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="inconnu"
|
||||
name="gender"
|
||||
checked={participation === "inconnu"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
-
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="oui"
|
||||
name="gender"
|
||||
checked={participation === "oui"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
Oui
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="non"
|
||||
name="gender"
|
||||
checked={participation === "non"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
Non
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="peut-etre"
|
||||
name="gender"
|
||||
checked={participation === "peut-etre"}
|
||||
onChange={onChangeValue2}
|
||||
/>{" "}
|
||||
Je ne sais pas encore
|
||||
</label>
|
||||
{participation === "peut-etre" ? (
|
||||
<div>
|
||||
On te le reproposera dans quelques temps.
|
||||
@ -149,6 +164,212 @@ Rejoindre les 86 bénévoles déjà présents sur le serveur se fait en cliquant
|
||||
Tu n'y es absolument pas obligé(e) ! C'est juste plus pratique.
|
||||
*/
|
||||
|
||||
const [acceptsNotifs, setAcceptsNotifs] = useState("")
|
||||
const [notifMessage, setNotifMessage] = useState("")
|
||||
|
||||
const mounted = useRef(false)
|
||||
useEffect(() => {
|
||||
if (mounted.current) {
|
||||
return
|
||||
}
|
||||
mounted.current = true
|
||||
if (!isNode) {
|
||||
if (volunteerNotifs?.acceptsNotifs === "oui") {
|
||||
navigator.serviceWorker.ready.then((registration) =>
|
||||
registration.pushManager.getSubscription().then((existedSubscription) => {
|
||||
const doesAcceptNotifs =
|
||||
_.isEqual(
|
||||
JSON.parse(JSON.stringify(existedSubscription)),
|
||||
JSON.parse(volunteerNotifs?.pushNotifSubscription)
|
||||
) && volunteerNotifs?.acceptsNotifs === "oui"
|
||||
setAcceptsNotifs(doesAcceptNotifs ? "oui" : "non")
|
||||
})
|
||||
)
|
||||
} else {
|
||||
setAcceptsNotifs("non")
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onChangePushNotifs = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
event.preventDefault()
|
||||
if (isNode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return
|
||||
}
|
||||
|
||||
const isChecked = event.target.value === "oui"
|
||||
if (!isChecked) {
|
||||
setNotifMessage("")
|
||||
setAcceptsNotifs("non")
|
||||
dispatch(
|
||||
fetchVolunteerNotifsSet(jwt, 0, {
|
||||
acceptsNotifs: "non",
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
|
||||
if (!registration.pushManager) {
|
||||
setNotifMessage(
|
||||
"Il y a un problème avec le push manager. Il faudrait utiliser un navigateur plus récent !"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const convertedVapidKey = urlBase64ToUint8Array(
|
||||
process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY
|
||||
)
|
||||
|
||||
function urlBase64ToUint8Array(base64String?: string) {
|
||||
if (!base64String) {
|
||||
return ""
|
||||
}
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
|
||||
// eslint-disable-next-line
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/")
|
||||
|
||||
const rawData = atob(base64)
|
||||
const outputArray = new Uint8Array(rawData.length)
|
||||
|
||||
for (let i = 0; i < rawData.length; i += 1) {
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
return outputArray
|
||||
}
|
||||
|
||||
if (!convertedVapidKey) {
|
||||
console.error("No convertedVapidKey available")
|
||||
}
|
||||
|
||||
try {
|
||||
const existedSubscription = await registration.pushManager.getSubscription()
|
||||
|
||||
if (existedSubscription === null) {
|
||||
// No subscription detected, make a request
|
||||
try {
|
||||
const newSubscription = await registration.pushManager.subscribe({
|
||||
applicationServerKey: convertedVapidKey,
|
||||
userVisibleOnly: true,
|
||||
})
|
||||
// New subscription added
|
||||
if (
|
||||
volunteerNotifs?.acceptsNotifs === "oui" &&
|
||||
!subscriptionEqualsSave(
|
||||
newSubscription,
|
||||
volunteerNotifs?.pushNotifSubscription
|
||||
)
|
||||
) {
|
||||
setNotifMessage(
|
||||
"Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera."
|
||||
)
|
||||
} else {
|
||||
setNotifMessage("C'est enregistré !")
|
||||
}
|
||||
|
||||
setAcceptsNotifs("oui")
|
||||
dispatch(
|
||||
fetchVolunteerNotifsSet(jwt, 0, {
|
||||
pushNotifSubscription: JSON.stringify(newSubscription),
|
||||
acceptsNotifs: "oui",
|
||||
})
|
||||
)
|
||||
} catch (_e) {
|
||||
if (Notification.permission !== "granted") {
|
||||
setNotifMessage(
|
||||
"Mince tu as bloqué toutes notifications pour le site des bénévoles, l'exact opposé de ce qu'il fallait :) Pour annuler ça, les instructions sont ici : https://support.pushcrew.com/support/solutions/articles/9000098467-how-to-unblock-notifications-from-a-website-that-you-once-blocked-"
|
||||
)
|
||||
} else {
|
||||
setNotifMessage(
|
||||
"Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Existed subscription detected
|
||||
if (
|
||||
volunteerNotifs?.acceptsNotifs === "oui" &&
|
||||
!subscriptionEqualsSave(
|
||||
existedSubscription,
|
||||
volunteerNotifs?.pushNotifSubscription
|
||||
)
|
||||
) {
|
||||
setNotifMessage(
|
||||
"Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera."
|
||||
)
|
||||
} else {
|
||||
setNotifMessage("C'est enregistré !")
|
||||
}
|
||||
|
||||
setAcceptsNotifs("oui")
|
||||
dispatch(
|
||||
fetchVolunteerNotifsSet(jwt, 0, {
|
||||
pushNotifSubscription: JSON.stringify(existedSubscription),
|
||||
acceptsNotifs: "oui",
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (_e) {
|
||||
setNotifMessage(
|
||||
"Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !"
|
||||
)
|
||||
}
|
||||
},
|
||||
[dispatch, jwt, volunteerNotifs]
|
||||
)
|
||||
|
||||
function subscriptionEqualsSave(toCheck: PushSubscription, save: string | undefined): boolean {
|
||||
if (!save) {
|
||||
return !toCheck
|
||||
}
|
||||
return _.isEqual(JSON.parse(JSON.stringify(toCheck)), JSON.parse(save))
|
||||
}
|
||||
|
||||
if (notifs.length === 0) {
|
||||
notifs.push(
|
||||
<div key="pushNotifs">
|
||||
<div className={styles.notificationsPage}>
|
||||
<div className={styles.notificationsContent}>
|
||||
<div className={styles.formLine} key="line-participation">
|
||||
<label>
|
||||
Acceptes-tu d'être notifié(e) ici quand on aura des questions
|
||||
ou informations importantes pour toi ?
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="oui"
|
||||
name="gender"
|
||||
checked={acceptsNotifs === "oui"}
|
||||
onChange={onChangePushNotifs}
|
||||
/>{" "}
|
||||
Oui
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="non"
|
||||
name="gender"
|
||||
checked={acceptsNotifs === "non"}
|
||||
onChange={onChangePushNotifs}
|
||||
/>{" "}
|
||||
Non
|
||||
</label>
|
||||
</label>
|
||||
<div className={styles.message}>{notifMessage}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: React.SyntheticEvent): void => {
|
||||
event.preventDefault()
|
||||
@ -166,6 +387,10 @@ Tu n'y es absolument pas obligé(e) ! C'est juste plus pratique.
|
||||
</div>
|
||||
)
|
||||
|
||||
if (volunteerNotifs === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div>{notifs.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}</div>
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@
|
||||
|
||||
.notificationsContent {
|
||||
@include page-content-wrapper;
|
||||
|
||||
background-color: #ece3df;
|
||||
}
|
||||
|
||||
.notifIntro {
|
||||
@ -19,6 +21,7 @@
|
||||
label {
|
||||
display: block;
|
||||
margin-left: 5px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
select,
|
||||
input {
|
||||
@ -34,6 +37,12 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 10px;
|
||||
color: rgb(11, 138, 0);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 10px;
|
||||
color: rgb(255, 0, 0);
|
||||
|
@ -12,20 +12,14 @@ export type Props = RouteComponentProps
|
||||
|
||||
const HomePage: FC<Props> = (): JSX.Element => {
|
||||
const dispatch = useDispatch()
|
||||
const readyStatus = useSelector((state: AppState) => state.volunteerNotifsSet.readyStatus)
|
||||
const volunteerNotifs = useSelector(
|
||||
(state: AppState) => state.volunteerNotifsSet.entity,
|
||||
shallowEqual
|
||||
)
|
||||
|
||||
const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual)
|
||||
const jwt = useSelector((state: AppState) => state.auth.jwt, shallowEqual)
|
||||
|
||||
if (!readyStatus || readyStatus === "idle" || readyStatus === "request")
|
||||
return <p>Loading...</p>
|
||||
if (jwt === undefined) return <p>Loading...</p>
|
||||
|
||||
if (jwt) {
|
||||
return <Notifications dispatch={dispatch} jwt={jwt} volunteerNotifs={volunteerNotifs} />
|
||||
return <Notifications dispatch={dispatch} jwt={jwt} />
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
|
@ -8,7 +8,8 @@ import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadshee
|
||||
|
||||
const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json")
|
||||
|
||||
const REMOTE_UPDATE_DELAY = 20000
|
||||
const REMOTE_UPDATE_DELAY = 40000
|
||||
const DELAY_AFTER_QUERY = 1000
|
||||
|
||||
export type ElementWithId<ElementNoId> = { id: number } & ElementNoId
|
||||
|
||||
@ -26,14 +27,6 @@ export const sheetNames = new SheetNames()
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type SheetList = { [sheetName in keyof SheetNames]?: Sheet<object, ElementWithId<object>> }
|
||||
const sheetList: SheetList = {}
|
||||
setInterval(
|
||||
() =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
Object.values(sheetList).forEach((sheet: Sheet<object, ElementWithId<object>>) =>
|
||||
sheet.dbUpdate()
|
||||
),
|
||||
REMOTE_UPDATE_DELAY
|
||||
)
|
||||
|
||||
export function getSheet<
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
@ -48,7 +41,14 @@ export function getSheet<
|
||||
sheetList[sheetName] = new Sheet<ElementNoId, Element>(sheetName, specimen, translation)
|
||||
}
|
||||
|
||||
return sheetList[sheetName] as Sheet<ElementNoId, Element>
|
||||
const sheet = sheetList[sheetName] as Sheet<ElementNoId, Element>
|
||||
|
||||
setTimeout(
|
||||
() => setInterval(() => sheet.dbUpdate(), REMOTE_UPDATE_DELAY),
|
||||
1000 * Object.values(sheetList).length
|
||||
)
|
||||
|
||||
return sheet
|
||||
}
|
||||
|
||||
export class Sheet<
|
||||
@ -83,7 +83,7 @@ export class Sheet<
|
||||
(englishProp: string) => (specimen as any)[englishProp]
|
||||
) as Element
|
||||
|
||||
this.dbLoad()
|
||||
setTimeout(() => this.dbLoad(), 100 * Object.values(sheetList).length)
|
||||
}
|
||||
|
||||
async getList(): Promise<Element[] | undefined> {
|
||||
@ -198,6 +198,8 @@ export class Sheet<
|
||||
if (!row) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sheet.addRow(stringifiedRow)
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayDBAccess()
|
||||
} else {
|
||||
const keys = Object.keys(stringifiedRow)
|
||||
const sameCells = _.every(keys, (key: keyof Element) => {
|
||||
@ -211,6 +213,8 @@ export class Sheet<
|
||||
})
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await row.save()
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayDBAccess()
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,6 +226,8 @@ export class Sheet<
|
||||
if (rows[rowToDelete]) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await rows[rowToDelete].delete()
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayDBAccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -233,6 +239,7 @@ export class Sheet<
|
||||
|
||||
// Load sheet into an array of objects
|
||||
const rows = (await sheet.getRows()) as StringifiedElement[]
|
||||
await delayDBAccess()
|
||||
const elements: Element[] = []
|
||||
if (!rows[0]) {
|
||||
throw new Error(`No column types defined in sheet ${this.name}`)
|
||||
@ -495,3 +502,7 @@ function parseDate(value: string): Date {
|
||||
}
|
||||
return new Date(+matchDate[1], +matchDate[2] - 1, +matchDate[3])
|
||||
}
|
||||
|
||||
async function delayDBAccess(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, DELAY_AFTER_QUERY))
|
||||
}
|
||||
|
@ -112,6 +112,8 @@ export const volunteerNotifsSet = expressAccessor.set(
|
||||
adult: newVolunteer.adult,
|
||||
active: newVolunteer.active,
|
||||
hiddenNotifs: newVolunteer.hiddenNotifs,
|
||||
pushNotifSubscription: newVolunteer.pushNotifSubscription,
|
||||
acceptsNotifs: newVolunteer.acceptsNotifs,
|
||||
} as VolunteerNotifs,
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
volunteerForgot,
|
||||
} from "./gsheets/volunteers"
|
||||
import config from "../config"
|
||||
import notificationsSubscribe from "./notificationsSubscribe"
|
||||
|
||||
const app = express()
|
||||
|
||||
@ -70,6 +71,9 @@ app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
|
||||
// UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet)
|
||||
app.post("/VolunteerNotifsSet", secure as RequestHandler, volunteerNotifsSet)
|
||||
|
||||
// Push notification subscription
|
||||
app.post("/notifications/subscribe", notificationsSubscribe)
|
||||
|
||||
// Use React server-side rendering middleware
|
||||
app.get("*", ssr)
|
||||
|
||||
|
29
src/server/notificationsSubscribe.ts
Normal file
29
src/server/notificationsSubscribe.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Request, Response, NextFunction } from "express"
|
||||
import webpush from "web-push"
|
||||
|
||||
const publicKey = process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY
|
||||
const privateKey = process.env.FORCE_ORANGE_PRIVATE_VAPID_KEY
|
||||
if (publicKey && privateKey) {
|
||||
webpush.setVapidDetails("mailto: contact@parisestludique.fr", publicKey, privateKey)
|
||||
}
|
||||
|
||||
export default function notificationsSubscribe(
|
||||
request: Request,
|
||||
response: Response,
|
||||
_next: NextFunction
|
||||
): void {
|
||||
const subscription = request.body
|
||||
|
||||
console.log(subscription)
|
||||
|
||||
const payload = JSON.stringify({
|
||||
title: "Hello!",
|
||||
body: "It works.",
|
||||
})
|
||||
webpush
|
||||
.sendNotification(subscription, payload)
|
||||
.then((result) => console.log(result))
|
||||
.catch((e) => console.log(e.stack))
|
||||
|
||||
response.status(200).json({ success: true })
|
||||
}
|
@ -31,6 +31,37 @@ export default (
|
||||
${extractor.getStyleTags()}
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.isSubscribed = false;
|
||||
window.swRegistration = null;
|
||||
|
||||
//REGISTER AND UNREGISTER SERVICE WORKER
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
window.addEventListener('load', function() {
|
||||
var swPath = '/service-worker.js';
|
||||
navigator.serviceWorker.register(swPath)
|
||||
.then(function(registration) {
|
||||
console.log('Service Worker registered');
|
||||
window.swRegistration = registration;
|
||||
registration.pushManager.getSubscription()
|
||||
.then(function(subscription) {
|
||||
console.log("subscription", JSON.stringify(subscription));
|
||||
window.isSubscribed = !(subscription === null);
|
||||
|
||||
if (window.isSubscribed) {
|
||||
console.log('User IS subscribed.');
|
||||
} else {
|
||||
console.log('User is NOT subscribed.');
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log('Service Worker registration failed: ', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Insert the router, which passed from server-side -->
|
||||
<div id="react-view">${htmlContent}</div>
|
||||
|
||||
|
@ -28,6 +28,10 @@ export class Volunteer {
|
||||
password1 = ""
|
||||
|
||||
password2 = ""
|
||||
|
||||
pushNotifSubscription = ""
|
||||
|
||||
acceptsNotifs = ""
|
||||
}
|
||||
|
||||
export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
||||
@ -45,6 +49,8 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
||||
created: "creation",
|
||||
password1: "passe1",
|
||||
password2: "passe2",
|
||||
pushNotifSubscription: "pushNotifSubscription",
|
||||
acceptsNotifs: "accepteLesNotifs",
|
||||
}
|
||||
|
||||
const elementName = "Volunteer"
|
||||
@ -64,6 +70,8 @@ export const volunteerExample: Volunteer = {
|
||||
created: new Date(0),
|
||||
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||
pushNotifSubscription: "",
|
||||
acceptsNotifs: "",
|
||||
}
|
||||
|
||||
export const emailRegexp =
|
||||
@ -97,6 +105,8 @@ export interface VolunteerNotifs {
|
||||
adult: number
|
||||
active: string
|
||||
hiddenNotifs: number[]
|
||||
pushNotifSubscription: string
|
||||
acceptsNotifs: string
|
||||
}
|
||||
export const volunteerNotifsSet =
|
||||
serviceAccessors.securedCustomPost<[number, Partial<VolunteerNotifs>]>("NotifsSet")
|
||||
|
8
src/types/index.d.ts
vendored
8
src/types/index.d.ts
vendored
@ -3,7 +3,6 @@ declare const __SERVER__: boolean
|
||||
declare const __DEV__: boolean
|
||||
declare const __LOCAL__: boolean
|
||||
declare const __TEST__: boolean
|
||||
declare const __SENDGRID_API_KEY__: string
|
||||
|
||||
declare module "*.svg"
|
||||
declare module "*.gif"
|
||||
@ -21,10 +20,15 @@ declare namespace NodeJS {
|
||||
__DEV__: boolean
|
||||
__LOCAL__: boolean
|
||||
__TEST__: boolean
|
||||
__SENDGRID_API_KEY__: string
|
||||
$RefreshReg$: () => void
|
||||
$RefreshSig$$: () => void
|
||||
}
|
||||
|
||||
interface ProcessEnv {
|
||||
SENDGRID_API_KEY?: string
|
||||
FORCE_ORANGE_PUBLIC_VAPID_KEY?: string
|
||||
FORCE_ORANGE_PRIVATE_VAPID_KEY?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
@ -125,4 +125,28 @@ const config = (isWeb = false): Configuration => ({
|
||||
},
|
||||
})
|
||||
|
||||
export function getClientEnvironment(allowedKeys: string[]): any {
|
||||
const raw = Object.keys(process.env)
|
||||
// Custom regex to allow only a certain category of variables available to the application
|
||||
.filter((key) => allowedKeys.indexOf(key) >= 0)
|
||||
.reduce(
|
||||
(env: any, key: string) => {
|
||||
env[key] = process.env[key]
|
||||
return env
|
||||
},
|
||||
{
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
}
|
||||
)
|
||||
// Stringify all values so we can feed into Webpack DefinePlugin
|
||||
const stringified = {
|
||||
"process.env": Object.keys(raw).reduce((env: any, key: string) => {
|
||||
env[key] = JSON.stringify(raw[key])
|
||||
return env
|
||||
}, {}),
|
||||
}
|
||||
|
||||
return stringified
|
||||
}
|
||||
|
||||
export default config
|
||||
|
@ -7,7 +7,7 @@ import CompressionPlugin from "compression-webpack-plugin"
|
||||
import ImageMinimizerPlugin from "image-minimizer-webpack-plugin"
|
||||
import merge from "webpack-merge"
|
||||
|
||||
import baseConfig, { isDev } from "./base.config"
|
||||
import baseConfig, { isDev, getClientEnvironment } from "./base.config"
|
||||
|
||||
const getPlugins = () => {
|
||||
let plugins = [
|
||||
@ -16,6 +16,7 @@ const getPlugins = () => {
|
||||
filename: isDev ? "[name].css" : "[name].[contenthash].css",
|
||||
chunkFilename: isDev ? "[id].css" : "[id].[contenthash].css",
|
||||
}),
|
||||
new webpack.DefinePlugin(getClientEnvironment(["FORCE_ORANGE_PUBLIC_VAPID_KEY"])),
|
||||
]
|
||||
|
||||
if (isDev)
|
||||
|
66
yarn.lock
66
yarn.lock
@ -1590,6 +1590,11 @@
|
||||
dependencies:
|
||||
postcss "5 - 7"
|
||||
|
||||
"@types/detect-node@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/detect-node/-/detect-node-2.0.0.tgz#696e024ddd105c72bbc6a2e3f97902a2886f2c3f"
|
||||
integrity sha512-+BozjlbPTACYITf1PWf62HLtDV79HbmZosUN1mv1gGrnjDCRwBXkDKka1sf6YQJvspmfPXVcy+X6tFW62KteeQ==
|
||||
|
||||
"@types/eslint-scope@^3.7.0":
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e"
|
||||
@ -1956,6 +1961,11 @@
|
||||
"@types/mime" "^1"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/serviceworker@^0.0.32":
|
||||
version "0.0.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/serviceworker/-/serviceworker-0.0.32.tgz#c262fed4e394f7f3e3787a597f8ecb5c12161626"
|
||||
integrity sha512-3B1uuyZQ86m9C3BBhNfl4mJs+Whdm1ozPyeLm1Z5hX4cKH0JLPfD9CBFEfdXmCMeNdbcJGfBZXYNXzlrpVrwQg==
|
||||
|
||||
"@types/source-list-map@*":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
|
||||
@ -1997,6 +2007,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||
|
||||
"@types/web-push@^3.3.2":
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15"
|
||||
integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/webpack-bundle-analyzer@^4.4.1":
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz#bcc2501be10c8cdd1d170bc6b7847b3321f20440"
|
||||
@ -2600,6 +2617,16 @@ arrify@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
|
||||
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
|
||||
|
||||
asn1.js@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
|
||||
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
|
||||
dependencies:
|
||||
bn.js "^4.0.0"
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
||||
@ -2909,6 +2936,11 @@ bl@^1.0.0:
|
||||
readable-stream "^2.3.5"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
bn.js@^4.0.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
|
||||
body-parser@1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||
@ -3987,6 +4019,11 @@ detect-newline@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
||||
|
||||
detect-node@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
||||
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
||||
|
||||
diff-sequences@^26.6.2:
|
||||
version "26.6.2"
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
|
||||
@ -5747,6 +5784,13 @@ http-signature@~1.2.0:
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
http_ece@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75"
|
||||
integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==
|
||||
dependencies:
|
||||
urlsafe-base64 "~1.0.0"
|
||||
|
||||
https-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
||||
@ -7593,6 +7637,11 @@ mini-css-extract-plugin@^2.1.0:
|
||||
dependencies:
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
minimalistic-assert@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||
|
||||
minimatch@^3.0.4, minimatch@~3.0.2:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
@ -10844,6 +10893,11 @@ url-to-options@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
|
||||
integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=
|
||||
|
||||
urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6"
|
||||
integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY=
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
@ -10952,6 +11006,18 @@ watchpack@^2.2.0:
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.1.2"
|
||||
|
||||
web-push@^3.4.5:
|
||||
version "3.4.5"
|
||||
resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1"
|
||||
integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g==
|
||||
dependencies:
|
||||
asn1.js "^5.3.0"
|
||||
http_ece "1.1.0"
|
||||
https-proxy-agent "^5.0.0"
|
||||
jws "^4.0.0"
|
||||
minimist "^1.2.5"
|
||||
urlsafe-base64 "^1.0.0"
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
|
Loading…
x
Reference in New Issue
Block a user