Source of truth: docs/issues/23-app-result-delivery.md in this repository (see PR #1 until merged). If this issue and the draft diverge, the draft wins.
Summary
Replace Issue 20's temporary alerts with the final delivery UX: success/empty/error
notifications via UNUserNotificationCenter (with settings deep-link actions), plus
optional auto-save through FileSink.
Context
DESIGN §9.5 defines delivery; Issue 04 provides remediationURL; Issue 11 provides
FileSink. KU-7 flags notification reliability for unsigned dev builds.
Scope
App/ScanMD/Delivery/NotificationCenter+ScanMD.swift, DeliveryHandler.swift;
CaptureCoordinator delivering-phase rewrite.
Detailed Requirements
- Authorization: request
UNUserNotificationCenter authorization (.alert) lazily —
on first delivery, not at launch (DESIGN §10.3 spirit). Denied → fall back to a brief
NSAlert-free path: menu bar icon flashes success (SF Symbol swap for 1.5 s) and
errors fall back to NSAlert (errors must never be silent).
- Success: clipboard write (always, via
ClipboardSink) → notification title
Copied as Markdown, body = first 120 chars of the markdown (sanitized to a single
line, ellipsis) — note: notification content is user-visible by design; do NOT log it
(p1 still applies to logs).
.emptyResult: title No text found, body names the source kind.
- Errors: title
Capture failed, body = userMessage. If remediationURL != nil,
attach a notification category with action button Open System Settings that opens
the URL (UNNotificationAction; handle in the delegate; app is LSUIElement so set
the delegate on launch).
- Auto-save (
app.autoSave && output.directory != nil): run FileSink in directory
mode after the clipboard write; success appends · saved as <basename> to the
notification body; failure sends a separate error notification (clipboard content
is intact — say so in the body: Result is still on the clipboard).
- Notification identifiers: one mutable delivery notification per capture
(scanmd.capture.<uuid>); remove delivered ones older than the last 5 (no center
spam).
- Tests:
DeliveryHandler behind UserNotifying + sink fakes — matrix: success/
empty/error×(remediation yes/no)×(autosave off/on/success/fail) asserting
title/body/action/category and sink calls. Delegate action-routing unit-tested with a
fake response.
- Manual checklist (KU-7): notifications appear for success/empty/error on a signed
dev build (ad-hoc ok; if unsigned build drops notifications, record it per KU-7
and validate on the signed build) · action button opens the exact Settings pane ·
auto-save writes the templated file and the body shows the name · auto-save failure
(read-only folder) keeps clipboard + shows the follow-up error · Focus/DND behavior
noted.
Acceptance Criteria
Validation
swift test --filter DeliveryTests + manual checklist with screenshots of each
notification kind.
Dependencies
Issues 11, 20.
Non-goals
Capture history UI, "Save As…" notification action (v2), sounds/badges.
Design References
DESIGN §9.5, §8.6, §10.2-p1, §10.3; ISSUE_PLAN KU-7.
Summary
Replace Issue 20's temporary alerts with the final delivery UX: success/empty/error
notifications via
UNUserNotificationCenter(with settings deep-link actions), plusoptional auto-save through
FileSink.Context
DESIGN §9.5 defines delivery; Issue 04 provides
remediationURL; Issue 11 providesFileSink. KU-7 flags notification reliability for unsigned dev builds.Scope
App/ScanMD/Delivery/NotificationCenter+ScanMD.swift,DeliveryHandler.swift;CaptureCoordinatordelivering-phase rewrite.Detailed Requirements
UNUserNotificationCenterauthorization (.alert) lazily —on first delivery, not at launch (DESIGN §10.3 spirit). Denied → fall back to a brief
NSAlert-free path: menu bar icon flashes success (SF Symbol swap for 1.5 s) anderrors fall back to
NSAlert(errors must never be silent).ClipboardSink) → notification titleCopied as Markdown, body = first 120 chars of the markdown (sanitized to a singleline, ellipsis) — note: notification content is user-visible by design; do NOT log it
(p1 still applies to logs).
.emptyResult: titleNo text found, body names the source kind.Capture failed, body =userMessage. IfremediationURL != nil,attach a notification category with action button
Open System Settingsthat opensthe URL (
UNNotificationAction; handle in the delegate; app isLSUIElementso setthe delegate on launch).
app.autoSave && output.directory != nil): runFileSinkin directorymode after the clipboard write; success appends
· saved as <basename>to thenotification body; failure sends a separate error notification (clipboard content
is intact — say so in the body:
Result is still on the clipboard).(
scanmd.capture.<uuid>); remove delivered ones older than the last 5 (no centerspam).
DeliveryHandlerbehindUserNotifying+ sink fakes — matrix: success/empty/error×(remediation yes/no)×(autosave off/on/success/fail) asserting
title/body/action/category and sink calls. Delegate action-routing unit-tested with a
fake response.
dev build (ad-hoc ok; if unsigned build drops notifications, record it per KU-7
and validate on the signed build) · action button opens the exact Settings pane ·
auto-save writes the templated file and the body shows the name · auto-save failure
(read-only folder) keeps clipboard + shows the follow-up error · Focus/DND behavior
noted.
Acceptance Criteria
NSAlertremains only in denied-notification error fallback).Validation
swift test --filter DeliveryTests+ manual checklist with screenshots of eachnotification kind.
Dependencies
Issues 11, 20.
Non-goals
Capture history UI, "Save As…" notification action (v2), sounds/badges.
Design References
DESIGN §9.5, §8.6, §10.2-p1, §10.3; ISSUE_PLAN KU-7.