Skip to content

Commit

Permalink
enhance: automate push notifications and implement unsubscribe functi…
Browse files Browse the repository at this point in the history
…onality (#3574)
  • Loading branch information
BShaq committed May 25, 2023
1 parent 344fbf2 commit b6deca5
Show file tree
Hide file tree
Showing 37 changed files with 652 additions and 235 deletions.
2 changes: 1 addition & 1 deletion apps/backend-docker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"scripts": {
"dev": "npm-run-all --parallel dev:build dev:run",
"dev:build": "tsup --watch",
"dev:run": "nodemon -w dist/ -w '../../packages/graphql/dist' --exec 'node -r dotenv/config dist/index.js'",
"dev:run": "nodemon -w dist/ -w '../../packages/graphql/dist' --exec 'node -r dotenv/config ./dist/index.js'",
"build": "cross-env NODE_ENV=production tsup",
"build:test": "npm run build",
"format": "prettier --write .",
Expand Down
3 changes: 1 addition & 2 deletions apps/frontend-control/worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ self.addEventListener('install', () => {

// Reload each open page to make sure the new service worker is in charge
self.addEventListener('activate', () => {
self.clients.matchAll({ type: 'window' })
.then(tabs => {
self.clients.matchAll({ type: 'window' }).then((tabs) => {
tabs.forEach((tab) => {
tab.navigate(tab.url)
})
Expand Down
1 change: 0 additions & 1 deletion apps/frontend-manage/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,3 @@ if (process.env.NODE_ENV !== 'test') {
} else {
module.exports = nextConfig
}

75 changes: 37 additions & 38 deletions apps/frontend-manage/public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
{
"theme_color": "#0028a5",
"background_color": "#ffffff",
"display": "standalone",
"scope": "/",
"start_url": "/",
"name": "KlickerUZH Manage Application",
"short_name": "KlickerUZH Manage",
"description": "Open Source Audience Interaction",
"orientation": "portrait",
"prefer_related_applications": false,
"icons": [
{
"src": "/manifest-icon-192.maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/manifest-icon-192.maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

"theme_color": "#0028a5",
"background_color": "#ffffff",
"display": "standalone",
"scope": "/",
"start_url": "/",
"name": "KlickerUZH Manage Application",
"short_name": "KlickerUZH Manage",
"description": "Open Source Audience Interaction",
"orientation": "portrait",
"prefer_related_applications": false,
"icons": [
{
"src": "/manifest-icon-192.maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/manifest-icon-192.maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
4 changes: 1 addition & 3 deletions apps/frontend-manage/worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ self.addEventListener('install', () => {

// Reload each open page to make sure the new service worker is in charge
self.addEventListener('activate', () => {
self.clients.matchAll({ type: 'window' })
.then(tabs => {
self.clients.matchAll({ type: 'window' }).then((tabs) => {
tabs.forEach((tab) => {
tab.navigate(tab.url)
})
})
})

3 changes: 2 additions & 1 deletion apps/frontend-pwa/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ NEXT_PUBLIC_IMAGE_BASE_PATH="https://$S3_HOSTNAME/klicker-prod/img"
NEXT_PUBLIC_ADD_RESPONSE_URL="http://127.0.0.1:7072/api/AddResponse"
NEXT_PUBLIC_API_URL="http://127.0.0.1:3000/api/graphql"
NEXT_PUBLIC_API_URL_SSR=$NEXT_PUBLIC_API_URL
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY="BFTs2KshUdSwCqQgZzaaCZK7h3L4pd8hjJoLk1jv1-iPN9c-_7JdLkQi5IV_k3Ml4rrx12HOkcANwVk39L_RTOM"
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY="BKgVsZFXdusxzKMrOTIJiAtiou8STnRcwbv_nvlseghjAUAlfmWKpCKBcfY_D-DH9PPYBunrJRh4uNeNF7a3TU4"


APP_SECRET=abcd

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-pwa/src/components/CourseElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function CourseElement({
!course.isSubscribed && !pushDisabled && 'cursor-pointer'
),
}}
disabled={course.isSubscribed || !!pushDisabled}
disabled={!!pushDisabled}
onClick={() => {
if (disabled) return
onSubscribeClick(course.isSubscribed, course.id)
Expand Down
142 changes: 65 additions & 77 deletions apps/frontend-pwa/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,74 @@ import {
ParticipationsDocument,
Session,
SubscribeToPushDocument,
UnsubscribeFromPushDocument,
} from '@klicker-uzh/graphql/dist/ops'
import { H1, UserNotification } from '@uzh-bf/design-system'
import dayjs from 'dayjs'
import { useTranslations } from 'next-intl'
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import usePushNotifications from 'shared-components/src/hooks/usePushNotifications'
import CourseElement from '../components/CourseElement'
import Layout from '../components/Layout'
import LinkButton from '../components/common/LinkButton'
import {
determineInitialSubscriptionState,
subscribeParticipant,
} from '../utils/push'

const Index = function () {
const t = useTranslations()
const [pushDisabled, setPushDisabled] = useState<boolean | null>(null)
const [userInfo, setUserInfo] = useState<string>('')
const [registration, setRegistration] =
useState<ServiceWorkerRegistration | null>(null)
const [subscription, setSubscription] = useState<PushSubscription | null>(
null
)

const { data, loading, error } = useQuery(ParticipationsDocument, {
variables: { endpoint: subscription?.endpoint },
fetchPolicy: 'network-only',
})
const [subscribeToPush] = useMutation(SubscribeToPushDocument)
const [unsubscribeFromPush] = useMutation(UnsubscribeFromPushDocument)

// This is necessary to make sure navigator is defined
useEffect(() => {
determineInitialSubscriptionState().then(({ disabled, info, reg, sub }) => {
setPushDisabled(disabled)
setUserInfo(info)
setRegistration(reg)
setSubscription(sub)
async function subscribeUser(
subscriptionObject: PushSubscription,
courseId: string
) {
await subscribeToPush({
variables: {
subscriptionObject,
courseId,
},
refetchQueries: [
{
query: ParticipationsDocument,
variables: { endpoint: subscriptionObject.endpoint },
},
],
})
}, [])
}

const [subscribeToPush] = useMutation(SubscribeToPushDocument)
async function unsubscribeUser(
subscriptionObject: PushSubscription,
courseId: string
) {
await unsubscribeFromPush({
variables: {
courseId,
endpoint: subscriptionObject.endpoint,
},
refetchQueries: [
{
query: ParticipationsDocument,
variables: { endpoint: subscriptionObject.endpoint },
},
],
})
}

const {
userInfo,
setUserInfo,
subscription,
subscribeUserToPush,
unsubscribeUserFromPush,
} = usePushNotifications({
subscribeToPush: subscribeUser,
unsubscribeFromPush: unsubscribeUser,
})

const { data, loading } = useQuery(ParticipationsDocument, {
variables: { endpoint: subscription?.endpoint },
fetchPolicy: 'network-only',
})

const {
courses,
Expand Down Expand Up @@ -147,62 +176,21 @@ const Index = function () {
return <div>loading...</div>
}

async function onSubscribeClick(subscribed: boolean, courseId: string) {
async function onSubscribeClick(
isSubscribedToPush: boolean,
courseId: string
) {
setUserInfo('')
// Case 1: User unsubscribed
if (subscribed) {
// TODO: updateSubscriptionOnServer(subscription, courseId)
// Case 2: User subscribed
} else {
// Case 2a: User already has a push subscription
if (subscription) {
subscribeToPush({
variables: {
subscriptionObject: subscription,
courseId,
},
})
// Case 2b: User has no push subscription yet
console.log('onSubscribeClick')
try {
if (isSubscribedToPush) {
await unsubscribeUserFromPush(courseId)
} else {
try {
const newSubscription = await subscribeParticipant(
registration!,
courseId
)
setSubscription(newSubscription)
subscribeToPush({
variables: {
subscriptionObject: newSubscription,
courseId,
},
})
} catch (e) {
console.error(e)
// Push notifications are disabled
if (Notification.permission === 'denied') {
setPushDisabled(true)
setUserInfo(
`Sie haben Push-Benachrichtigungen für diese Applikation deaktiviert. Wenn Sie Push-Benachrichtigungen
abonnieren möchten, aktivieren Sie diese in Ihrem Browser und laden Sie die Seite neu.`
)
// User has clicked away the prompt without allowing nor blocking
} else if (
e instanceof DOMException &&
e.name === 'NotAllowedError'
) {
setUserInfo(
'Bitte erlauben Sie Push-Benachrichtigungen für diese Applikation in Ihrem Browser.'
)
// An error occured
} else {
setUserInfo(
'Beim Versuch, Sie für Push-Benachrichtigungen zu registrieren, ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.'
)
}
}
await subscribeUserToPush(courseId)
}
} catch (error) {
console.error('An error occurred while un/subscribing a user: ', error)
}
return subscription
}

return (
Expand Down
3 changes: 1 addition & 2 deletions apps/frontend-pwa/worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ self.addEventListener('install', () => {

// Reload each open page to make sure the new service worker is in charge
self.addEventListener('activate', () => {
self.clients.matchAll({ type: 'window' })
.then(tabs => {
self.clients.matchAll({ type: 'window' }).then((tabs) => {
tabs.forEach((tab) => {
tab.navigate(tab.url)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ metadata:
data:
API_DOMAIN: {{ .Values.backendGraphql.apiDomain | quote }}
NOTIFICATION_URL: {{ .Values.backendGraphql.notifications.url | quote }}
NOTIFICATION_SUPPORT_EMAIL: {{ .Values.backendGraphql.notifications.supportEmail | quote }}
COOKIE_DOMAIN: {{ .Values.backendGraphql.cookieDomain | quote }}
REDIS_CACHE_HOST: {{ .Values.backendGraphql.redisCache.host | quote }}
REDIS_CACHE_PORT: {{ .Values.backendGraphql.redisCache.port | quote }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,21 @@ spec:
- POST
- -H
- "Content-Type: application/json"
- -H
- "x-token: {{ .Values.cron.token }}"
- -H
- "x-graphql-yoga-csrf: updateGroupAverageScores"
- -d
- '{"query":"mutation { updateGroupAverageScores }"}'
- >
{
"operationName": "UpdateGroupAverageScores",
"variables": {},
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "fccda9bacb2fb5740670ff2b65ef9258bc41a0ff97e5be4a94f523669fa4f46d"
}
}
}
- "http://{{ include "chart.fullname" . }}-backend-graphql:3000/api/graphql"
restartPolicy: OnFailure
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "chart.fullname" . }}-cron-push-notifications
labels:
{{- include "chart.labels" . | nindent 4 }}
spec:
schedule: {{ .Values.cron.pushNotifications | quote }}
jobTemplate:
spec:
template:
spec:
containers:
- name: curl
image: curlimages/curl:7.85.0
imagePullPolicy: IfNotPresent
args:
- -X
- POST
- -H
- "Content-Type: application/json"
- -H
- "x-token: {{ .Values.cron.token }}"
- -H
- "x-graphql-yoga-csrf: sendPushNotifications"
- -d
- >
{
"operationName": "SendPushNotifications",
"variables": {},
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "d0a02157dafcbc7c4df31ea01934d39ca72f9e71eb2ae75cf37298898db99416"
}
}
}
- 'http://{{ include "chart.fullname" . }}-backend-graphql:3000/api/graphql'
restartPolicy: OnFailure
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type: Opaque
stringData:
APP_SECRET: {{ .Values.appSecret | quote }}
NOTIFICATION_SECRET: {{ .Values.backendGraphql.notifications.secret | quote }}
VAPID_PUBLIC_KEY: {{ .Values.backendGraphql.vapid.publicKey | quote }}
VAPID_PRIVATE_KEY: {{ .Values.backendGraphql.vapid.privateKey | quote }}
DATABASE_URL: {{ .Values.backendGraphql.databaseUrl | quote }}
REDIS_CACHE_PASS: {{ .Values.backendGraphql.redisCache.pass | quote }}
REDIS_PASS: {{ .Values.backendGraphql.redisExec.pass | quote }}

0 comments on commit b6deca5

Please sign in to comment.