Magic Apps

Documentation

macmagic.app

Native notifications

Both apps post banners through the Swift helper rather than Electron, because the helper is what gives a notification its bundle identity and its action buttons. What each app announces, and when, is its own: Magimail's arrival and watermark logic is in ../magimail/notifications.md, Magical's alert scheduler and invite watcher in ../magical/notifications.md.

Helper posts the banner

MagicalHelper/MagimailHelper owns UNUserNotificationCenter and posts every banner. Dispatch is a sendToHelper call over the newline-delimited JSON-RPC in process-model-and-ipc.md. When the helper is absent, as it is under pnpm dev, dispatch silently does nothing.

Electron's own Notification does support macOS action buttons, so the often-repeated claim that the buttons would have to be dropped is wrong. What Electron does not expose is content.filterCriteria, the per-account hook a macOS Focus Filter matches on; staying in Swift keeps that capability without a second process, since the helper already owns the menu bar and the settings window.

Registration happens at launch

The main process sends notify.setup immediately after spawning the helper, which registers the action categories and requests authorization. Left to the first dispatch, macOS would know nothing about the app until a notification arrived: there would be no row in System Settings to grant beforehand, and the first notification would necessarily be missed.

Bundle identity

UNUserNotificationCenter aborts without a bundle identity, so the helper must run from Contents/MacOS inside the app and carry the app's signing identifier rather than one derived from its filename. A helper elsewhere in the bundle, or signed under its own identifier, either aborts or fails requestAuthorization with UNErrorDomain Code=1, and the app never appears in System Settings, Notifications. Under pnpm dev the helper runs outside any bundle, so check banners on the installed build.

The signing rules and signIgnore are in building-and-signing.md; the identity the widget extension needs, and the quit that follows from sharing an identity, are in process-model-and-ipc.md; the Focus filter's identity requirement is in focus-filters.md.

Category and action contract

Every banner belongs to a registered UNNotificationCategory, and the actions are declared on the category rather than per notification.

Content and formatting

macOS lays a banner out as title, then subtitle, then body, and the order is not the order the fields are named in. A subject therefore has to be the subtitle to appear above a snippet; assigning the snippet to the subtitle renders it above the subject and in bold, which reads as the subject being a preview of itself.

Magimail's mapping is: sender name as title, subject as subtitle, snippet as body; when the snippet is empty the subject moves into the body rather than being printed twice.

Magical's mapping is: an alert uses the meeting name, then In 5 minutes · 14:30, then the conference provider or the location. An invitation uses the event name, then Invitation from <organiser>, then when it is.

Sound belongs to the category level

The loud level sets content.sound and the silent level does not. willPresent honours the same level, so a silent category stays silent when the app is frontmost; a global switch on top would only raise the question of which wins. macOS System Settings remains the final say.