Skip to content

clients: S-D29 client half — native alert delivery, notification.* keys, permission at first use #439

Description

@justin13888

The core half of S-D29 landed in #411: capsule-core::notify is the shared decision function
(evaluate(&NotifyInput, now) for the classes true at an instant, next_deadline(&NotifyInput, now)
for the one instant to arm), and capsule-sdk::ffi exports both as free functions with RFC 3339
timestamps. S-D29 stays ready because nothing on a device evaluates it yet: every predicate
input is caller-supplied, since the core holds none of the trigger state.

This is the remainder — the delivery half, which is the larger one.

Owed:

  • Native scheduling and presentation. iOS UNUserNotificationCenter with
    UNCalendarNotificationTrigger / UNTimeIntervalNotificationTrigger; Android
    NotificationManagerCompat scheduled via AlarmManager.setExactAndAllowWhileIdle (or
    WorkManager where inexact timing is acceptable). No abstraction layer — the design says
    native APIs directly.
  • The arm / re-arm / cancel loop, per client. Call next_alert_deadline after any state
    change and cancel-then-arm if the value moved. next_alert_deadline withholds a deadline
    whenever the alert would not certainly be true on arrival, so an armed notification never
    needs re-checking at fire time — which is the whole point, since it fires with the app not
    running.
  • The three server-state classes surface at next app launch. quota_soft,
    quota_grace_expiring, quarantine_pending and drop_pending are not pre-armable, by
    design. Closing that gap is the post-v1 wake tier, not this issue.
  • The client's own state, wired into FfiNotifyInput. Nothing persists the fields today:
    the last completed sync instant and the un-synced count, the last GET /v1/quota state, the
    quarantine-surface count, and the pending-drop count. RecoveryCadence::notify_facts already
    covers the recovery half.
  • Snooze and disable, per class. They ride FfiNotifyInput.suppressed_until (class wire
    name to an instant, exclusive), so the client owns the bounded-snooze-then-badge state and
    the core does not get a second copy of a mechanic the recovery cadence already implements.
    Disabling suppresses the warning and never the behavior.
  • The notification.* catalog keys in locales/, one per class, with the parameters the
    predicate emits (count, days_behind, grace, snooze_budget). Blocked until now by the
    i18n guard, which requires a live consumer — that consumer is this issue.
  • Permission at first use. Android POST_NOTIFICATIONS from API 33 and the iOS
    authorization prompt, requested the first time a class has something to say rather than at
    launch, so the prompt carries context. A denial degrades every class to its in-app badge:
    no error, no retry, no failed flow.
  • Validation. The pre-arm lifecycle under a mocked clock (arm on deadline-known, re-arm on
    the change that moves it, cancel when it clears, never two live timers per class); the snooze
    bound degrading to a badge; a refused authorization degrading to a badge without error; and
    the per-platform smoke that fires a pre-armed alert with the app terminated — the one gate
    the core half could not run.

Contract: capsule-docs/src/content/docs/design/notifications.md (Delivery, Shared Alert
Mechanics, Validation). Ledger: the S-D29 row and detail block in SLICES.md.

Refs #411

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions