ios: notifications, actionable approvals, Live Activity, and the relay push path
Brings the phone's ambient surfaces (UX_IOS §5/§8) to maturity: - Notification pipeline (NotificationRouter): local notifications for approvals and needs-input transitions while backgrounded; low-risk approvals are actionable from the banner (Allow requires device auth, high-risk must open the app's Face ID gate); taps deep-link to the session; app-icon badge = NEEDS YOU count; resolutions withdraw the notification (first-responder-wins). The relay's content-free approval.pending tickle localizes via Localizable.strings. - Live Activity: new NucleicRemoteWidgets extension target (lock screen + Dynamic Island) rendering one aggregate Activity — N running / M waiting + the most urgent session — started/updated/ ended by LiveActivityManager as session state changes. - Out-of-band push path: nucleic-edge gains POST /v1/push/register (admin) so the host can upload tokens for LAN-only pairings; the host's new PushRelayClient (config-gated on NUCLEIC_RELAY_URL + NUCLEIC_RELAY_ADMIN_SECRET) mirrors Hello.pushToken to the relay and wakes non-connected phones when an approval arrives, throttled per device. Everything stays off until the relay is provisioned. Worker tests (23) and Swift suites pass apart from the pre-existing fixture gaps and nvrsion flake. Simulated APNs delivery is blocked in this environment (notification auth can't be granted headlessly). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -12,6 +12,9 @@ struct NucleicRemoteApp: App {
|
||||
.environmentObject(store)
|
||||
.onAppear {
|
||||
store.onAppear()
|
||||
// Notification categories + delegate must be in place before any
|
||||
// notification can arrive, so actions/taps always route.
|
||||
NotificationRouter.shared.install(store: store)
|
||||
// Surface the notifications prompt + register for APNS. The token rides along
|
||||
// in the sync Hello; the relay uses it to wake the phone for approvals (§3).
|
||||
// Skipped in demo mode so offline UI previews aren't blocked by the system dialog.
|
||||
@@ -21,8 +24,10 @@ struct NucleicRemoteApp: App {
|
||||
// Anonymous, opt-out, once-a-day DAI heartbeat (docs/CLOUD_INFRA.md §4).
|
||||
Task { await HeartbeatReporter.reportIfDue() }
|
||||
}
|
||||
// Re-check on every foreground so a new active day is counted.
|
||||
// Re-check on every foreground so a new active day is counted; keep the store's
|
||||
// activity flag current so it only notifies for background transitions.
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
store.setScenePhaseActive(phase == .active)
|
||||
if phase == .active { Task { await HeartbeatReporter.reportIfDue() } }
|
||||
}
|
||||
}
|
||||
@@ -64,6 +69,11 @@ struct RootView: View {
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) { ErrorBubble() }
|
||||
// A notification tap routes to its session: jump to the Sessions tab, where
|
||||
// `SessionsView` consumes `pendingRoute` and pushes the detail.
|
||||
.onChange(of: store.pendingRoute) { _, route in
|
||||
if route != nil { tab = 1 }
|
||||
}
|
||||
.tint(Palette.accent)
|
||||
.preferredColorScheme((AppAppearance(rawValue: appearanceRaw) ?? .system).colorScheme)
|
||||
.dynamicTypeSize((AppTextSize(rawValue: textSizeRaw) ?? .medium).dynamicTypeSize)
|
||||
|
||||
Reference in New Issue
Block a user