Skip to content

Blazor: Push notification UI and service worker for scheduled expenses #673

@maraf

Description

@maraf

Overview

Add Blazor client-side push notification support: service worker handlers, JS interop, subscription synchronization, and a notification settings page. Follows the Recollections baseline (neptuo/Recollections#395) adapted for Money's Blazor PWA architecture.

Service Worker — Push Event Handling

Update service-worker.published.js

Add push notification event listeners:

self.addEventListener('push', event => event.waitUntil(onPush(event)));
self.addEventListener('notificationclick', event => event.waitUntil(onNotificationClick(event)));

async function onPush(event) {
    const data = event.data?.json() || {};
    await self.registration.showNotification(data.title || 'Money', {
        body: data.body || 'You have scheduled expenses due today.',
        icon: '/icon-192.png',
        badge: '/icon-badge-96.png',
        tag: data.tag || 'money-expense-template',
        renotify: true,
        data: { url: data.url || '/expense-templates' }
    });
}

async function onNotificationClick(event) {
    event.notification.close();
    const url = event.notification.data?.url || '/\;
    const clients = await self.clients.matchAll({ type: 'window' });
    for (const client of clients) {
        if (client.url.includes(url) && 'focus' in client) return client.focus();
    }
    if (self.clients.openWindow) return self.clients.openWindow(url);
}

Badge Asset

  • Add icon-badge-96.png (monochrome 96×96) for Android notification badge

JS Interop Layer

New JS Module (wwwroot/js/notifications.js)

Money.Notifications = {
    isSupported: () => 'serviceWorker' in navigator && 'PushManager' in window,
    getPermission: () => Notification.permission,
    getTimeZone: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
    getSubscription: async () => { /* from SW registration */ },
    subscribe: async (publicKey) => { /* PushManager.subscribe with applicationServerKey */ },
    unsubscribe: async () => { /* get subscription, unsubscribe() */ }
};

C# Interop (Money.Blazor.Host/Services/PushNotificationInterop.cs)

  • IsSupportedAsync() — Check browser Push API support
  • GetPermissionAsync() — Returns "granted" | "denied" | "default"
  • GetTimeZoneAsync() — Browser timezone for preferred hour calculation
  • GetSubscriptionAsync() — Current push subscription (if any)
  • SubscribeAsync(publicKey) — Request browser permission + subscribe
  • UnsubscribeAsync() — Revoke browser subscription

Subscription Synchronizer

NotificationSubscriptionSynchronizer.cs

  • On app start: auto-restore subscription if permission granted + localStorage flag set
  • On explicit enable: subscribe browser → send to API → set localStorage flag
  • On disable: unsubscribe browser → send revoke to API → clear flag
  • On app update (version change): re-subscribe to refresh endpoint

Notification Settings Page

New Page: /notifications

File: Money.Blazor.Host/Pages/Notifications.razor

UI Elements:

  • Global notifications toggle (IsEnabled)
  • "Scheduled expenses" section:
    • Toggle (IsEnabled)
    • Preferred time picker (hour selector, 0-23)
    • Timezone display (auto-detected, read-only)
  • Browser subscription status:
    • "Enable on this browser" button (if permission not granted)
    • "Subscribed ✓" indicator (if active)
    • "Notifications blocked" warning (if permission denied by browser)

CQRS Integration:

  • Dispatches SetNotificationSettings, SetExpenseTemplateNotificationSettings, SubscribePushNotification, UnsubscribePushNotification
  • Queries GetNotificationSettings on page load

Navigation

Add link to notification settings from user menu/settings area.

Client-Side Registration

  • Register PushNotificationInterop and NotificationSubscriptionSynchronizer in Program.cs
  • Add notification command/query URLs to client-side CommandMapper/QueryMapper

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions