Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m22s
129 lines
3.9 KiB
JavaScript
129 lines
3.9 KiB
JavaScript
/* Service Worker for Chorly push notifications */
|
|
|
|
/* Activate immediately — no waiting for old tabs to close */
|
|
self.addEventListener('install', function (event) {
|
|
event.waitUntil(self.skipWaiting())
|
|
})
|
|
|
|
self.addEventListener('activate', function (event) {
|
|
event.waitUntil(self.clients.claim())
|
|
})
|
|
|
|
self.addEventListener('push', function (event) {
|
|
if (!event.data) return
|
|
|
|
let payload
|
|
try {
|
|
payload = event.data.json()
|
|
} catch (e) {
|
|
payload = { title: 'Chorly', body: event.data.text() }
|
|
}
|
|
|
|
const title = payload.title || 'Chorly'
|
|
// Android Chrome has a bug where event.action is unreliable — only show Approve
|
|
// so tapping it always executes the correct action.
|
|
const isAndroidChrome =
|
|
/Android/.test(self.navigator.userAgent) && /Chrome\//.test(self.navigator.userAgent)
|
|
// Expiry-warning notifications are informational only — no action buttons
|
|
const actions =
|
|
payload.type === 'chore_expiring_soon'
|
|
? []
|
|
: isAndroidChrome
|
|
? [{ action: 'approve', title: 'Approve' }]
|
|
: [
|
|
{ action: 'approve', title: 'Approve' },
|
|
{ action: 'deny', title: 'Reject' },
|
|
]
|
|
const options = {
|
|
body: payload.body || '',
|
|
icon: '/icons/icon-192x192.png',
|
|
data: payload,
|
|
actions,
|
|
}
|
|
|
|
event.waitUntil(self.registration.showNotification(title, options))
|
|
})
|
|
|
|
self.addEventListener('notificationclick', function (event) {
|
|
event.notification.close()
|
|
|
|
const payload = event.notification.data || {}
|
|
const approveToken = payload.approve_token
|
|
const denyToken = payload.deny_token
|
|
const childId = payload.child_id
|
|
const entityId = payload.entity_id
|
|
const entityType = payload.entity_type
|
|
|
|
function navigateOrOpen(url) {
|
|
return clients
|
|
.matchAll({ type: 'window', includeUncontrolled: true })
|
|
.then(function (clientList) {
|
|
const sameOriginClients = clientList.filter(function (c) {
|
|
return new URL(c.url).origin === self.location.origin
|
|
})
|
|
if (sameOriginClients.length > 0) {
|
|
const client = sameOriginClients[0]
|
|
return client.focus().then(function () {
|
|
return client.navigate(url)
|
|
})
|
|
}
|
|
return clients.openWindow(url)
|
|
})
|
|
}
|
|
|
|
function executeDigestAction(token) {
|
|
return fetch('/api/digest-action/' + token, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
})
|
|
.then(function (res) {
|
|
if (res.ok) {
|
|
// Action executed — navigate to parent view to show the result
|
|
var successUrl =
|
|
childId && entityId && entityType
|
|
? '/parent/' + childId + '?scrollTo=' + entityId + '&entityType=' + entityType
|
|
: '/parent'
|
|
return navigateOrOpen(successUrl)
|
|
}
|
|
if (res.status === 401) {
|
|
// Not authenticated — navigate with digestToken so the PIN flow executes the action
|
|
var authUrl =
|
|
childId && entityId && entityType
|
|
? '/parent/' +
|
|
childId +
|
|
'?digestToken=' +
|
|
token +
|
|
'&scrollTo=' +
|
|
entityId +
|
|
'&entityType=' +
|
|
entityType
|
|
: '/parent'
|
|
return navigateOrOpen(authUrl)
|
|
}
|
|
// Token invalid/used/expired — navigate to parent
|
|
return navigateOrOpen(childId ? '/parent/' + childId : '/parent')
|
|
})
|
|
.catch(function () {
|
|
return navigateOrOpen('/parent')
|
|
})
|
|
}
|
|
|
|
if (event.action === 'approve' && approveToken) {
|
|
event.waitUntil(executeDigestAction(approveToken))
|
|
return
|
|
}
|
|
|
|
if (event.action === 'deny' && denyToken) {
|
|
event.waitUntil(executeDigestAction(denyToken))
|
|
return
|
|
}
|
|
|
|
// Body tap — open or focus the app then navigate to the deep link
|
|
const deepLinkUrl =
|
|
childId && entityId && entityType
|
|
? '/parent/' + childId + '?scrollTo=' + entityId + '&entityType=' + entityType
|
|
: '/parent'
|
|
|
|
event.waitUntil(navigateOrOpen(deepLinkUrl))
|
|
})
|