Add push notification functionality with tests and digest scheduler
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m14s

- Implemented push subscription API with tests for subscribing and unsubscribing users.
- Created web push notification tests triggered by child actions.
- Added digest scheduler to send email digests to users at 9 PM local time.
- Developed utility functions for creating and validating digest action tokens.
- Integrated web push sender to handle sending notifications to users.
- Added service worker for handling push notifications in the frontend.
- Created a push opt-in component for user notification preferences.
- Implemented tests for the push opt-in component to ensure correct behavior.
- Updated frontend services to manage push subscriptions and permissions.
This commit is contained in:
2026-04-15 21:56:10 -04:00
parent 0d50a324a3
commit ad2bdf4c4f
47 changed files with 3177 additions and 197 deletions

View File

@@ -8,6 +8,9 @@ import {
isParentPersistent,
logoutParent,
logoutUser,
consumePendingReturnUrl,
hasPendingReturnUrl,
clearPendingReturnUrl,
} from '../../stores/auth'
import { getCachedImageUrl, getCachedImageBlob } from '@/common/imageCache'
import '@/assets/styles.css'
@@ -105,6 +108,7 @@ const close = () => {
show.value = false
error.value = ''
stayInParentMode.value = false
clearPendingReturnUrl()
}
const submit = async () => {
@@ -136,7 +140,8 @@ const submit = async () => {
// Authenticate parent and navigate
authenticateParent(stayInParentMode.value)
close()
router.push('/parent')
const returnUrl = consumePendingReturnUrl()
router.push(returnUrl || '/parent')
} catch (e) {
error.value = 'Network error'
}
@@ -253,6 +258,9 @@ onMounted(() => {
eventBus.on('profile_updated', fetchUserProfile)
document.addEventListener('mousedown', handleClickOutside)
fetchUserProfile()
if (!isParentAuthenticated.value && hasPendingReturnUrl()) {
open()
}
})
onUnmounted(() => {

View File

@@ -206,6 +206,7 @@ onBeforeUnmount(() => {
props.getItemClass?.(item),
{ 'item-ready': props.readyItemId === item.id },
]"
:data-item-id="item.id"
:ref="(el) => (itemRefs[item.id] = el)"
@click.stop="handleClicked(item)"
>

View File

@@ -32,6 +32,9 @@ vi.mock('../../../stores/auth', () => ({
isParentPersistent: isParentPersistentRef,
logoutParent: vi.fn(),
logoutUser: vi.fn(),
consumePendingReturnUrl: vi.fn(() => null),
hasPendingReturnUrl: vi.fn(() => false),
clearPendingReturnUrl: vi.fn(),
}))
global.fetch = vi.fn()