Skip to content
Draft
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6f77419
feat(live-activities): Phase 1 — data model, registry actor, and Swif…
rwarner Mar 18, 2026
19fa56b
feat(live-activities): Phase 2 — notification routing, token reportin…
rwarner Mar 18, 2026
76ad81f
feat(live-activities): Phase 3 — push-to-start token observation and …
rwarner Mar 18, 2026
310b8fb
feat(live-activity): Phase 4 — settings UI and privacy disclosure
rwarner Mar 18, 2026
2e93408
refactor(live-activity): address code review findings
rwarner Mar 19, 2026
072be06
refactor(live-activity): localization, remove availability wrapper, i…
rwarner Mar 19, 2026
d4b7a99
fix(live-activity): address security and performance review findings
rwarner Mar 19, 2026
f2e3e97
style: apply SwiftFormat to live activity files
rwarner Mar 19, 2026
729d1da
chore: remove plan doc from branch (docs/plans/ is local-only)
rwarner Mar 19, 2026
887e69a
chore: ignore docs/plans/ (local planning artifacts)
rwarner Mar 19, 2026
7c48aa2
Revert "chore: ignore docs/plans/ (local planning artifacts)"
rwarner Mar 19, 2026
3949c08
fix(live-activity): resolve build errors and register files in Xcode …
rwarner Mar 19, 2026
6f70ecf
feat(live-activity): add iOS Live Activities support
rwarner Mar 19, 2026
115077a
test(live-activity): add unit tests for Live Activity handlers
rwarner Mar 19, 2026
43673c1
fix(live-activity): address all 7 Copilot review comments
rwarner Mar 19, 2026
e99d4d9
fix(live-activity): address 3 more Copilot review comments
rwarner Mar 19, 2026
5fb61e6
Fix lint issues from bundle exec fastlane autocorrect
rwarner Mar 23, 2026
431e8f2
Handle Live Activity commands in foreground notifications and trigger…
rwarner Mar 23, 2026
f6aa154
Address code owner review: design system, color assets, cleanup
rwarner Mar 24, 2026
7f1ab64
Add wire-format contract tests for Live Activity frozen values
rwarner Mar 24, 2026
0f6e66b
Add contract tests for webhook dictionary keys (#14, #15)
rwarner Mar 24, 2026
dbf08df
Fix WebSocket local push path for Live Activities on Simulator
rwarner Mar 24, 2026
ec5481f
Address Copilot review feedback on Live Activity push handling
rwarner Mar 25, 2026
ed7e1f6
Fix Dynamic Island compact trailing clipping and stale date for timer…
rwarner Mar 25, 2026
5cd4826
Merge branch 'main' into feat/live-activities
rwarner Mar 25, 2026
5ce41a7
Fix SwiftFormat lint errors in live activity files
rwarner Mar 26, 2026
ff65853
Add beta label and TestFlight gate to Live Activities settings entry
rwarner Mar 26, 2026
f22b862
Use live_update: true instead of live_activity: true for iOS Live Act…
rwarner Mar 26, 2026
10878ee
Raise Live Activities minimum to iOS 17.2
rwarner Mar 26, 2026
25804fa
Merge branch 'main' into feat/live-activities
rwarner Mar 26, 2026
bd3b197
Fix Copilot review issues: ASCII tag validation, 17.2 availability gu…
rwarner Mar 31, 2026
71d937c
Simplify liveActivityRegistry: optional protocol property, no Any? ba…
rwarner Mar 31, 2026
f8c6a50
Revert unrelated SwiftFormat reflow of apis computed property
rwarner Mar 31, 2026
419ad5f
Remove allowsCustomMTLSCertificateImport, already removed in main (#4…
rwarner Mar 31, 2026
6671a96
Merge origin/main, resolve SettingsView conflict: use LabsLabel for l…
rwarner Mar 31, 2026
c2278b7
Merge branch 'main' into feat/live-activities
rwarner Apr 1, 2026
3bf9859
Simplify liveActivityRegistry to a single lazy var
rwarner Apr 1, 2026
199c4e1
Rename live activity dismissed webhook tag field to 'live_activity_tag'
rwarner Apr 1, 2026
56d1dd6
Merge branch 'main' into feat/live-activities
rwarner Apr 2, 2026
e9f4e3b
Merge branch 'main' into feat/live-activities
rwarner Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 91 additions & 36 deletions HomeAssistant.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions Sources/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// swiftlint:enable prohibit_environment_assignment

notificationManager.setupNotifications()
setupLiveActivityReattachment()
setupFirebase()
setupModels()
setupLocalization()
Expand Down Expand Up @@ -372,6 +373,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
})
}

private func setupLiveActivityReattachment() {
#if canImport(ActivityKit)
if #available(iOS 17.2, *) {
// Pre-warm the registry on the main thread before spawning background Tasks.
// This avoids a lazy-init race if a push notification handler accesses it
// concurrently from a background thread.
let registry = Current.liveActivityRegistry

Task {
// Re-attach observation tasks (push token + lifecycle) to any Live Activities
// that survived the previous process termination. Must run before the first
// notification handler fires so no push token updates are missed.
await registry.reattach()
}

// Begin observing the push-to-start token stream on a separate Task.
// The stream is infinite; this Task is kept alive for the app's lifetime.
Task {
await registry.startObservingPushToStartToken()
}
}
#endif
}

private func setupFirebase() {
let optionsFile: String = {
switch Current.appConfiguration {
Expand Down
26 changes: 26 additions & 0 deletions Sources/App/Notifications/NotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ import XCGLogger

class NotificationManager: NSObject, LocalPushManagerDelegate {
lazy var localPushManager: NotificationManagerLocalPushInterface = {
#if targetEnvironment(simulator)
return NotificationManagerLocalPushInterfaceDirect(delegate: self)
#else
if Current.isCatalyst {
return NotificationManagerLocalPushInterfaceDirect(delegate: self)
} else {
return NotificationManagerLocalPushInterfaceExtension()
}
#endif
}()

var commandManager = NotificationCommandManager()
Expand Down Expand Up @@ -281,6 +285,28 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
) {
Messaging.messaging().appDidReceiveMessage(notification.request.content.userInfo)

// Handle commands (including Live Activities) for foreground notifications.
// didReceiveRemoteNotification handles background pushes via Firebase/APNs,
// but willPresent fires when the app is in the foreground. Without this,
// notifications received while the app is open would never trigger the
// Live Activity handler.
// If a command is recognized, suppress the notification banner so the user
// sees only the Live Activity (not a duplicate standard notification).
if let hadict = notification.request.content.userInfo["homeassistant"] as? [String: Any],
(hadict["command"] as? String) != nil || (hadict["live_update"] as? Bool) == true {
commandManager.handle(notification.request.content.userInfo).done {
completionHandler([])
}.catch { error in
// Unknown command — fall through to normal banner presentation so the user isn't silently swallowed.
if case NotificationCommandManager.CommandError.unknownCommand = error {
completionHandler([.badge, .sound, .list, .banner])
} else {
completionHandler([])
}
}
return
}

if notification.request.content.userInfo[XCGLogger.notifyUserInfoKey] != nil,
UIApplication.shared.applicationState != .background {
completionHandler([])
Expand Down
4 changes: 4 additions & 0 deletions Sources/App/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@
<string>We use Siri to allow created shortcuts to interact with the app.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Used to dictate text to Assist.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>NSUserActivityTypes</key>
<array>
<string>AssistInAppIntent</string>
Expand Down
17 changes: 17 additions & 0 deletions Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,23 @@ This server requires a client certificate (mTLS) but the operation was cancelled
"kiosk.security.taps_required" = "Taps Required: %li";
"kiosk.title" = "Kiosk Mode";
"legacy_actions.disclaimer" = "Legacy iOS Actions are not the recommended way to interact with Home Assistant anymore, please use Scripts, Scenes and Automations directly in your Widgets, Apple Watch and CarPlay.";

"live_activity.title" = "Live Activities";
"live_activity.subtitle" = "Real-time Home Assistant updates on your Lock Screen and Dynamic Island.";
"live_activity.section.active" = "Active Activities";
"live_activity.section.status" = "Status";
"live_activity.section.privacy" = "Privacy";
"live_activity.empty_state" = "No active Live Activities";
"live_activity.status.enabled" = "Enabled";
"live_activity.status.not_supported" = "Not available on iPad";
"live_activity.status.open_settings" = "Open Settings";
"live_activity.end_all.button" = "End All Activities";
"live_activity.end_all.confirm.title" = "End all Live Activities?";
"live_activity.end_all.confirm.button" = "End All";
"live_activity.privacy.message" = "Live Activity content is visible on your Lock Screen and Dynamic Island without Face ID or Touch ID. Choose what you display carefully.";
"live_activity.frequent_updates.title" = "Frequent Updates";
"live_activity.frequent_updates.footer" = "Allows Home Assistant to update Live Activities up to once per second. Enable in Settings \u203A %@ \u203A Live Activities.";

"location_change_notification.app_shortcut.body" = "Location updated via App Shortcut";
"location_change_notification.background_fetch.body" = "Current location delivery triggered via background fetch";
"location_change_notification.beacon_region_enter.body" = "%@ entered via iBeacon";
Expand Down
Loading
Loading