Skip to content

Fix stale LA dismissed by iOS incorrectly blocking auto-restart#577

Merged
bjorkert merged 2 commits intoloopandlearn:live-activityfrom
achkars-org:fix/la-stale-foreground-refresh
Mar 26, 2026
Merged

Fix stale LA dismissed by iOS incorrectly blocking auto-restart#577
bjorkert merged 2 commits intoloopandlearn:live-activityfrom
achkars-org:fix/la-stale-foreground-refresh

Conversation

@MtlPhil
Copy link
Copy Markdown

@MtlPhil MtlPhil commented Mar 26, 2026

Problem

When iOS dismisses a Live Activity because its staleDate passed (the stale overlay scenario), laRenewalFailed is false. The state observer's else branch then fires and sets dismissedByUser = true, which permanently blocks all auto-restart paths — startFromCurrentState() has guard !dismissedByUser at the top.

Result: foregrounding the app after a stale overlay appeared did nothing; the user had to manually press Restart Live Activity in Settings.

Root cause

In attachStateObserver, the .dismissed case only distinguished user-swipes from iOS-forced dismissals by checking laRenewalFailed. But there's a second iOS-initiated path: iOS silently removes the activity when staleDate passes, without setting laRenewalFailed.

Fix

1. attachStateObserver — also check staleDatePassed (activity.content.staleDate <= Date()). Both renewalFailed and staleDatePassed are iOS-initiated dismissals; only a fresh-activity swipe should block auto-restart.

2. handleForeground() Task — reset dismissedByUser = false before calling startFromCurrentState(), guarding against a race where the state observer fires .dismissed during our own end() call (before its Task cancellation takes effect) and incorrectly sets the flag.

Testing

  1. Let a Live Activity run until it goes stale (stale overlay appears).
  2. Background the app.
  3. Foreground the app — Live Activity should auto-restart without needing the Restart button.

🤖 Generated with Claude Code

MtlPhil and others added 2 commits March 26, 2026 07:40
startIfNeeded() unconditionally reused any existing activity, which
meant that on cold start (app killed while stale overlay was showing)
willEnterForeground is never sent, handleForeground never runs, and
viewDidAppear → startFromCurrentState → startIfNeeded just rebinds to
the stale activity — leaving the overlay visible.

Fix: before reusing an existing activity in startIfNeeded(), check
whether its staleDate has passed or the renewal window is open. If so,
end it (awaited) and call startIfNeeded() again so a fresh activity
with a new 7.5h deadline is started.

Also add cancelRenewalFailedNotification() to handleForeground() so
the "Live Activity Expiring" system notification is dismissed whenever
the foreground restart path fires, not only via forceRestart().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When iOS dismisses a Live Activity because its staleDate passed (background
stale overlay case), laRenewalFailed is false, so the state observer's else
branch fired and set dismissedByUser=true — permanently blocking all
auto-restart paths (startFromCurrentState has guard !dismissedByUser).

Fix 1: attachStateObserver now checks staleDatePassed alongside laRenewalFailed;
both are iOS-initiated dismissals that should allow auto-restart.

Fix 2: handleForeground() Task resets dismissedByUser=false before calling
startFromCurrentState(), guarding against the race where the state observer
fires .dismissed during our own end() call before its Task cancellation takes
effect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bjorkert bjorkert merged commit 6de1ae0 into loopandlearn:live-activity Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants