Pre-existing and independent of #25643 (surfaced while reviewing it). Filing so the invariant is defended at the source even after the immediate trigger is removed.
Problem
AppUpdatePresenter guards one update path against double-presentation but not the other:
showBlockingUpdate(using:) bails if a BlockingUpdateViewController is already on top (AppUpdatePresenter.swift:34-39). ✅
showNotice(using:) — the flexible/optional update — just calls ActionDispatcher.dispatch(NoticeAction.post(notice)) with no such guard (AppUpdatePresenter.swift:12-31). ❌
So two checkForAppUpdates() runs in quick succession can each reach showNotice and queue two identical "update available" notices, shown back-to-back.
Why the existing throttles don't prevent it
AppUpdateCoordinator news up a fresh instance per call, so there's no instance-level dedup. The persisted guards — shouldFetchAppStoreInfo (last-fetched date, ~1 day) and shouldShowFlexibleUpdate (lastSeenFlexibleUpdateDate + interval) — are day-granularity frequency caps, not in-flight dedup, and they race: if two runs both read the dates as stale before either persists its update, both proceed to showNotice.
The concrete trigger today is #25643's double updateRemoteConfig() → double checkForAppUpdates() on cold launch; #25665 (a fetch rate limit) removes that trigger. But the missing guard is the real gap — any future path that calls checkForAppUpdates() twice, or two foregrounds racing, can still double-post. This issue is about defending the presentation, not the fetch, so it holds regardless of caller.
Proposed
Mirror the showBlockingUpdate guard on showNotice: don't post a flexible notice if an in-app-update notice is already showing/queued — e.g. tag the Notice and check the NoticeStore before posting, or a presenter/coordinator-level "already requested this run" flag.
Files
WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift — showNotice(using:) vs showBlockingUpdate(using:)
WordPress/Classes/Services/AppUpdate/AppUpdateCoordinator.swift — checkForAppUpdates() and the day-granularity throttles
Pre-existing and independent of #25643 (surfaced while reviewing it). Filing so the invariant is defended at the source even after the immediate trigger is removed.
Problem
AppUpdatePresenterguards one update path against double-presentation but not the other:showBlockingUpdate(using:)bails if aBlockingUpdateViewControlleris already on top (AppUpdatePresenter.swift:34-39). ✅showNotice(using:)— the flexible/optional update — just callsActionDispatcher.dispatch(NoticeAction.post(notice))with no such guard (AppUpdatePresenter.swift:12-31). ❌So two
checkForAppUpdates()runs in quick succession can each reachshowNoticeand queue two identical "update available" notices, shown back-to-back.Why the existing throttles don't prevent it
AppUpdateCoordinatornews up a fresh instance per call, so there's no instance-level dedup. The persisted guards —shouldFetchAppStoreInfo(last-fetched date, ~1 day) andshouldShowFlexibleUpdate(lastSeenFlexibleUpdateDate+ interval) — are day-granularity frequency caps, not in-flight dedup, and they race: if two runs both read the dates as stale before either persists its update, both proceed toshowNotice.Relationship to #25643 / #25665
The concrete trigger today is #25643's double
updateRemoteConfig()→ doublecheckForAppUpdates()on cold launch; #25665 (a fetch rate limit) removes that trigger. But the missing guard is the real gap — any future path that callscheckForAppUpdates()twice, or two foregrounds racing, can still double-post. This issue is about defending the presentation, not the fetch, so it holds regardless of caller.Proposed
Mirror the
showBlockingUpdateguard onshowNotice: don't post a flexible notice if an in-app-update notice is already showing/queued — e.g. tag theNoticeand check theNoticeStorebefore posting, or a presenter/coordinator-level "already requested this run" flag.Files
WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift—showNotice(using:)vsshowBlockingUpdate(using:)WordPress/Classes/Services/AppUpdate/AppUpdateCoordinator.swift—checkForAppUpdates()and the day-granularity throttles