You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Recover Live Activity after APNs 410 token expiry (#657)
A 6.1.0 user reported the Live Activity vanishing and refusing to come
back without a manual Restart. Trace: APNs returned 410 on the per-
activity push token at 04:42; handleExpiredToken ended the activity but
the eventual iOS .dismissed (4 h later, under the default dismissal
policy) was classified as a user swipe and locked dismissedByUser=true.
Root cause is two cooperating bugs around an app-initiated end():
- end() nulls `current` and clears laRenewBy. handleExpiredToken's
comment said "Activity will restart on next BG refresh via
refreshFromCurrentState()", but renewIfNeeded short-circuits when
current is nil and performRefresh's bind-existing path rebinds to the
just-ended activity. bind() then clears endingForRestart, so the late
.dismissed reads as renewBy=0 / renewalFailed=false / endingForRestart=
false — branch (c) "USER" in the classifier.
- The classifier had no way to recognize a stale observer firing for an
activity the app no longer tracks.
Fixes:
- handleExpiredToken drives the restart synchronously on iOS 17.2+
(attemptPushToStartCreate "expired-token"), so the orphaned post-410
state is short-lived and adoption of the fresh activity cancels the
old observer.
- performRefresh / update bind-existing only to activities in
.active state. Binding to an .ended/.dismissed corpse would clear
endingForRestart and re-attach an observer that only ever delivers
.dismissed.
- .dismissed classifier gains branch (d): if the dismissed activity is
not the one we currently track, log and take no action — only the
foreground LA can be user-swiped, so a stale-observer delivery for an
already-replaced activity must not latch dismissedByUser=true.
// (a) Our own restart — do nothing, Task handles the rest.
1398
1427
LogManager.shared.log(category:.general, message:"[LA] dismissed by self (endingForRestart) — restart in-flight, no action")
1399
1428
}elseif renewalFailed || pastDeadline {
1400
1429
// (b) iOS system force-dismiss — allow auto-restart on next foreground.
1401
1430
LogManager.shared.log(category:.general, message:"[LA] dismissed by iOS (renewalFailed=\(renewalFailed), pastDeadline=\(pastDeadline)) — auto-restart on next foreground")
1431
+
}elseif !wasCurrentActivity {
1432
+
// (d) Stale observer for an activity we no longer track (e.g. a
1433
+
// post-410 end whose iOS-side dismissal landed hours later).
1434
+
// Not a user swipe — no flags to set.
1435
+
LogManager.shared.log(category:.general, message:"[LA] dismissed by stale observer (id=\(activity.id) is not current) — no action")
1402
1436
}else{
1403
1437
// (c) User decision — cancel renewal intent, block auto-restart.
0 commit comments