Skip to content

Notifications

View raw .md
On this page

Introduction#

The notification UI moved to the magic_notifications package. This package no longer ships the list view, the preferences view, the controller behind them, or the bell dropdown; magic_notifications >= 0.1.0 does, under NotificationsListView, NotificationPreferencesView, NotificationPreferencesController and NotificationDropdown.

What stays here is what a published notifications package cannot know on its own: registerMagicStarterNotificationRoutes() owns the two paths (/notifications and /settings/notifications), mounts them inside the authenticated layout.app shell, and wraps magic_notifications's screens in this package's own page geometry (MSPageContainer) so they read like every other page in the app rather than spreading full-bleed. MagicStarterAppLayout owns the polling lifecycle and renders the bell.

The entire feature is still opt-in behind magic_starter.features.notifications. When enabled, the layout starts polling and renders the bell; the routes resolve through Notify.view, not MagicStarter.view.

Real-Time Polling#

Notification polling is managed automatically by MagicStarterAppLayout. When the authenticated layout mounts and the notifications feature is enabled, it calls Notify.startPolling() to begin fetching notifications at a regular interval. Polling stops when the layout is disposed:

dart
@override
void initState() {
  super.initState();

  if (MagicStarterConfig.hasNotificationFeatures()) {
    Notify.startPolling();
  }
}

@override
void dispose() {
  if (MagicStarterConfig.hasNotificationFeatures()) {
    Notify.stopPolling();
  }
  super.dispose();
}

Notify.startPolling() is idempotent — calling it multiple times has no effect. It also triggers an immediate fetch so the UI has data without waiting for the first interval tick.

Notification List#

NotificationsListView (from magic_notifications) is a full-page view that displays all notifications with server-side pagination. It supports mark-as-read, mark-all-as-read, delete, and navigation to the notification's action URL.

registerMagicStarterNotificationRoutes() mounts it under the key 'notifications.list' on Notify.view, wrapped in this package's page container, and renders it at MagicStarterConfig.notificationsRoute():

dart
MagicRoute.page(
  MagicStarterConfig.notificationsRoute(),
  () => Notify.view.make('notifications.list'),
).title('magic_starter.titles.notifications');

Deleting a notification#

The mount supplies the view's onDelete, which is what makes the per-row delete control render at all: NotificationsListView draws it only when that callback is non-null. It does not hand the callback straight to Notify.deleteNotification. A delete is destructive, irreversible and one tap away in a scrollable list, so the mount asks first, through this package's own MSConfirmDialog:

  • The dialog is shown against MagicRouter.instance.navigatorKey.currentContext, because neither the view registry nor onDelete provides a BuildContext. A null context refuses rather than deleting: nobody could have been asked.
  • MSConfirmDialog rather than Magic.confirm so the dialog follows MagicStarter.manager.modalTheme and the host's dark mode. Magic.confirm styles from view.confirm.* with light-mode fallbacks.
  • Copy comes from notifications.delete_confirm_title, notifications.delete_confirm_message, common.delete and common.cancel. All four ship in assets/stubs/install/en.stub, so an app scaffolded by starter:install has them; an app with a hand-written catalogue needs them added, or the dialog renders the raw keys.

To take the confirmation out, or to change what it asks, register your own screen for 'notifications.list' before the routes are mapped and pass whatever onDelete you want. That registration wins, as described above.

Each notification item resolves its icon through the notification type icon slot (see Notification Type Icons). When a notification is tapped, it is marked as read and the user is navigated to the notification's actionUrl.

Notification Preferences#

NotificationPreferencesView (from magic_notifications) displays a type-by-channel preference matrix fetched from the backend. Each notification type (e.g., "Monitor Down") shows its available channels (email, in-app, push) as toggle switches.

registerMagicStarterNotificationRoutes() mounts it under 'notifications.preferences', passing the one thing the notifications package cannot know: where "back" goes in this app.

dart
Notify.view.register(
  'notifications.preferences',
  () => NotificationPreferencesView(
    backRoute: MagicStarterConfig.settingsHubRoute(),
  ),
);

Both mounts also pass headerBuilder, which draws each screen's header as an MSPageHeader inside the screen's own scroll view, so the two pages follow host-owned page chrome like every other starter page: under MagicStarter.useHostPageChrome the inbox reports its title and keeps only its mark-all-as-read action, and the preferences report the settings hub as their back fallback. A screen you register yourself draws its own header unless you pass a headerBuilder too.

The matrix structure returned by the API:

json
{
  "monitor_down": {
    "label": "Monitor Down",
    "channels": {
      "mail":     { "enabled": true,  "locked": false },
      "database": { "enabled": true,  "locked": true },
      "push":     { "enabled": false, "locked": false }
    }
  }
}

Locked channels display a lock icon and their toggle is disabled — the backend enforces that certain channels cannot be turned off.

Since magic_notifications 0.3.0 the screen also opens with a bulk card above the matrix, one switch per channel applying to every type at once, backed by the batch shape PUT /notification-preferences with {preferences: [{type, channel, is_enabled}, ...]}. magic-starter-laravel has accepted that shape since 0.0.7; a hand-rolled endpoint that only takes the single shape answers 422 and the card is dead.

The notification screens read five notifications.* keys that no package supplies: bulk_title and bulk_description head the bulk card, delete names the list row's delete glyph for a screen reader, delete_failed is shown when the server refuses a delete, and channel_sms labels the SMS row. All five ship in assets/stubs/install/en.stub, so an app scaffolded by starter:install has them; an app with a hand-written catalogue adds them, or each renders as its own key.

Notification Dropdown#

NotificationDropdown (from magic_notifications, no longer MSNotificationDropdown) is a standalone widget, not a view, that renders a bell icon with a live unread badge. It uses StreamBuilder<List<DatabaseNotification>> to reactively display the current notification count. MagicStarterAppLayout mounts it with the same five callbacks it always passed:

dart
NotificationDropdown(
  notificationStream: Notify.notifications(),
  onMarkAsRead: (id) => Notify.markAsRead(id),
  onMarkAllAsRead: () => Notify.markAllAsRead(),
  onNotificationTap: (notification) =>
      MagicRoute.to(notification.actionUrl ?? '/'),
  onViewAll: () =>
      MagicRoute.to(MagicStarterConfig.notificationsRoute()),
)

The dropdown is rendered inside AppLayout's header and sidebar via the centralized _buildNotificationBell() helper. It only appears when MagicStarterConfig.hasNotificationFeatures() returns true.

Key behaviors:

State Display
Loading Bell icon with no badge, spinner in dropdown body
Error Bell icon with no badge, error icon in dropdown body
Empty Bell icon with no badge, "No notifications" empty state
Unread > 0 Bell icon with red badge (9+ overflow), notification list

When a notification is tapped, it is marked as read and the onNotificationTap callback fires. The dropdown closes automatically after tap.

Notification Type Icons#

MagicStarter.useNotificationTypeMapper(...) and the MagicStarterNotificationTypeMapper typedef are gone. Saying what a notification type looks like is magic_notifications's own slot now, and it answers the same question for the list screen and the bell at once:

dart
import 'package:magic_notifications/magic_notifications.dart';

Notify.view.slot(
  NotificationViewRegistry.typeIconSlotView,
  'monitor_down',
  (context) => WIcon(Icons.error_outline, className: 'text-lg text-red-500'),
);

Register the slot name 'default' to answer for every type you did not name individually:

dart
Notify.view.slot(
  NotificationViewRegistry.typeIconSlotView,
  'default',
  (context) => WIcon(Icons.info_outline, className: 'text-lg text-blue-500'),
);

Both the list view and the dropdown read Notify.view.buildTypeIcon(type, context), which checks the type's own slot first and falls back to 'default', so one registration call answers for both surfaces.

Overriding a Screen#

An app that wants to swap either screen no longer registers on MagicStarter.view; it registers on Notify.view:

dart
Notify.view.register(
  'notifications.list',
  () => const MyCustomNotificationsListView(),
);

The order does not matter. registerMagicStarterNotificationRoutes() installs its own wrapped screens only when nobody has chosen one for that key (Notify.view.hasOverride), so a host registration wins whether it runs before or after the routes are mapped.

The wording matters here: the key is never absent. Reading Notify.view is what seeds magic_notifications' own two defaults into the registry, so has(key) is true before anybody has decided anything, and gating on it would make this package skip every time and never apply the page geometry below. That is worth saying explicitly here, because the two calls land in different files: the installer injects the route mount into route_service_provider.dart while the scaffold points Notify.view work at AppServiceProvider, and which of those boots first is a property of the host's provider list rather than of anything this package can see.

The registry's shape is identical to MagicStarter.view's (register / has / make / slot / buildSlot / clear), so an app that already knew one knows the other.

Feature Gate#

All notification functionality is gated behind MagicStarterConfig.hasNotificationFeatures():

dart
if (MagicStarterConfig.hasNotificationFeatures()) {
  Notify.startPolling();
}

Enable it in your configuration:

dart
Config.set('magic_starter.features.notifications', true);

When disabled:

  • Notification routes are not registered (the registration function is a no-op)
  • The bell icon dropdown does not render in the app layout
  • Polling never starts
  • Calling notification route paths throws StateError

Related Links:

esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.