From 70079c84056a6ceee8e12cac5e771b3e09f993cc Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:26:38 -0700 Subject: [PATCH 1/2] fix: guard flexible update notice against double-posting AppUpdatePresenter.showNotice posted the flexible in-app-update notice with no already-shown guard, so two checkForAppUpdates() runs racing on cold launch could queue two identical notices back-to-back. Tag the notice and bail if a notice with that tag is already the current notice in NoticeStore, mirroring the existing showBlockingUpdate guard. Fixes #25666 --- RELEASE-NOTES.txt | 1 + .../Misc/AppUpdateCoordinatorTests.swift | 62 +++++++++++++++++++ .../AppUpdate/AppUpdatePresenter.swift | 24 ++++++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 1d4751b48e0f..2a2bee8b23b2 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,6 @@ 27.1 ----- +* [*] [internal] In-app updates: guard the flexible update notice against double-posting when two update checks race [#25666] 27.0 diff --git a/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift b/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift index 5a37e3d9b3ba..251857fa405d 100644 --- a/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift @@ -1,3 +1,4 @@ +import WordPressFlux import XCTest @testable import WordPress @@ -190,6 +191,67 @@ final class AppUpdateCoordinatorTests: XCTestCase { XCTAssertFalse(presenter.didShowNotice) XCTAssertTrue(presenter.didShowBlockingUpdate) } + + // MARK: - AppUpdatePresenter flexible notice guard + + func testShowNoticePostsExactlyOneFlexibleNotice() throws { + // Given + let dispatcher = ActionDispatcher() + let noticeStore = NoticeStore(dispatcher: dispatcher) + let presenter = AppUpdatePresenter(noticeStore: noticeStore, dispatcher: dispatcher) + let appStoreInfo = try makeAppStoreInfo() + + // When + presenter.showNotice(using: appStoreInfo) + + // Then + XCTAssertEqual(noticeStore.currentNotice?.tag, AppUpdatePresenter.flexibleUpdateNoticeTag) + } + + func testShowNoticeSuppressesSecondFlexibleNoticeWhileOneIsShowing() throws { + // Given + let dispatcher = ActionDispatcher() + let noticeStore = NoticeStore(dispatcher: dispatcher) + let presenter = AppUpdatePresenter(noticeStore: noticeStore, dispatcher: dispatcher) + let appStoreInfo = try makeAppStoreInfo() + presenter.showNotice(using: appStoreInfo) + let firstNotice = try XCTUnwrap(noticeStore.currentNotice) + + // When a second presentation races in while the first is still showing + presenter.showNotice(using: appStoreInfo) + + // Then the current notice is still the first one and nothing was queued + XCTAssertEqual(noticeStore.currentNotice, firstNotice) + // Dismissing the current notice leaves no queued duplicate behind + ActionDispatcher.dispatch(NoticeAction.dismiss, dispatcher: dispatcher) + XCTAssertNil(noticeStore.currentNotice) + } + + func testShowNoticeCanPostAgainAfterPreviousNoticeIsCleared() throws { + // Given + let dispatcher = ActionDispatcher() + let noticeStore = NoticeStore(dispatcher: dispatcher) + let presenter = AppUpdatePresenter(noticeStore: noticeStore, dispatcher: dispatcher) + let appStoreInfo = try makeAppStoreInfo() + presenter.showNotice(using: appStoreInfo) + XCTAssertNotNil(noticeStore.currentNotice) + + // When the first notice is cleared and a later legitimate cycle posts again + ActionDispatcher.dispatch(NoticeAction.dismiss, dispatcher: dispatcher) + XCTAssertNil(noticeStore.currentNotice) + presenter.showNotice(using: appStoreInfo) + + // Then the guard does not permanently latch + XCTAssertEqual(noticeStore.currentNotice?.tag, AppUpdatePresenter.flexibleUpdateNoticeTag) + } + + private func makeAppStoreInfo() throws -> AppStoreLookupResponse.AppStoreInfo { + let data = try Bundle.test.json(named: "app-store-lookup-response") + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let response = try decoder.decode(AppStoreLookupResponse.self, from: data) + return try XCTUnwrap(response.results.first) + } } private final class MockAppStoreSearchService: AppStoreSearchProtocol { diff --git a/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift b/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift index 5b8515a47460..7ef1f00f7398 100644 --- a/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift +++ b/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift @@ -9,7 +9,26 @@ protocol AppUpdatePresenterProtocol { } final class AppUpdatePresenter: AppUpdatePresenterProtocol { + /// Stable tag used to identify the flexible in-app-update notice so a second + /// presentation can be suppressed while one is already showing. + static let flexibleUpdateNoticeTag: Notice.Tag = "in-app-update-flexible" + + private let noticeStore: NoticeStore + private let dispatcher: ActionDispatcher + + init( + noticeStore: NoticeStore = StoreContainer.shared.notice, + dispatcher: ActionDispatcher = .global + ) { + self.noticeStore = noticeStore + self.dispatcher = dispatcher + } + func showNotice(using appStoreInfo: AppStoreLookupResponse.AppStoreInfo) { + guard noticeStore.currentNotice?.tag != Self.flexibleUpdateNoticeTag else { + // Don't post another flexible update notice if one is already showing + return + } let viewModel = AppStoreInfoViewModel(appStoreInfo) let notice = Notice( title: viewModel.title, @@ -17,7 +36,8 @@ final class AppUpdatePresenter: AppUpdatePresenterProtocol { feedbackType: .warning, style: InAppUpdateNoticeStyle(), actionTitle: viewModel.updateButtonTitle, - cancelTitle: viewModel.cancelButtonTitle + cancelTitle: viewModel.cancelButtonTitle, + tag: Self.flexibleUpdateNoticeTag ) { accepted in if accepted { WPAnalytics.track(.inAppUpdateAccepted, properties: ["type": "flexible"]) @@ -26,7 +46,7 @@ final class AppUpdatePresenter: AppUpdatePresenterProtocol { WPAnalytics.track(.inAppUpdateDismissed) } } - ActionDispatcher.dispatch(NoticeAction.post(notice)) + ActionDispatcher.dispatch(NoticeAction.post(notice), dispatcher: dispatcher) WPAnalytics.track(.inAppUpdateShown, properties: ["type": "flexible"]) } From 93f3e553dbe4de2bb36ab5983103e8cd480089d6 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:05:30 -0700 Subject: [PATCH 2/2] fix: throttle checkForAppUpdates instead of guarding the notice Adopt the reviewer-suggested approach: keep an in-memory last-check timestamp (AppUpdateCheckThrottle, 5 minute minimum interval, injectable together with a date provider for tests) so rapid duplicate checkForAppUpdates calls are skipped upstream. Revert the presenter-level notice-tag guard since the throttle makes it redundant. --- .../Misc/AppUpdateCoordinatorTests.swift | 182 +++++++++++------- .../AppUpdate/AppUpdateCoordinator.swift | 42 +++- .../AppUpdate/AppUpdatePresenter.swift | 27 +-- 3 files changed, 156 insertions(+), 95 deletions(-) diff --git a/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift b/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift index 251857fa405d..65339f3ac947 100644 --- a/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Misc/AppUpdateCoordinatorTests.swift @@ -1,21 +1,24 @@ -import WordPressFlux -import XCTest - @testable import WordPress +import XCTest final class AppUpdateCoordinatorTests: XCTestCase { - private let service = MockAppStoreSearchService() private let presenter = MockAppUpdatePresenter() private let remoteConfigStore = RemoteConfigStoreMock() + private var checkThrottle: AppUpdateCheckThrottle! + private var currentDateProvider: MockCurrentDateProvider! private var store: InMemoryUserDefaults! override func setUp() { store = InMemoryUserDefaults() - service.didLookup = false + checkThrottle = AppUpdateCheckThrottle() + currentDateProvider = MockCurrentDateProvider() + currentDateProvider.dateToReturn = Date(timeIntervalSince1970: 1_000_000) + service.lookupCount = 0 presenter.didShowNotice = false presenter.didShowBlockingUpdate = false presenter.didOpenAppStore = false + presenter.showNoticeCount = 0 super.setUp() } @@ -27,6 +30,8 @@ final class AppUpdateCoordinatorTests: XCTestCase { service: service, presenter: presenter, remoteConfigStore: remoteConfigStore, + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, isLoggedIn: true, isInAppUpdatesEnabled: false ) @@ -48,6 +53,8 @@ final class AppUpdateCoordinatorTests: XCTestCase { service: service, presenter: presenter, remoteConfigStore: remoteConfigStore, + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, isLoggedIn: false, isInAppUpdatesEnabled: true ) @@ -70,6 +77,8 @@ final class AppUpdateCoordinatorTests: XCTestCase { presenter: presenter, remoteConfigStore: remoteConfigStore, store: store, + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, isLoggedIn: false, isInAppUpdatesEnabled: true, delayInDays: Int.max @@ -93,6 +102,8 @@ final class AppUpdateCoordinatorTests: XCTestCase { presenter: presenter, remoteConfigStore: remoteConfigStore, store: store, + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, isJetpack: true, isLoggedIn: true, isInAppUpdatesEnabled: true @@ -116,6 +127,8 @@ final class AppUpdateCoordinatorTests: XCTestCase { presenter: presenter, remoteConfigStore: remoteConfigStore, store: store, + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, isJetpack: true, isLoggedIn: true, isInAppUpdatesEnabled: true @@ -140,6 +153,8 @@ final class AppUpdateCoordinatorTests: XCTestCase { presenter: presenter, remoteConfigStore: remoteConfigStore, store: store, + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, isJetpack: true, isLoggedIn: true, isInAppUpdatesEnabled: true @@ -155,7 +170,7 @@ final class AppUpdateCoordinatorTests: XCTestCase { XCTAssertFalse(presenter.didShowBlockingUpdate) // Reset service and presenter - service.didLookup = false + service.lookupCount = 0 presenter.didShowNotice = false // When we check for updates again @@ -168,99 +183,128 @@ final class AppUpdateCoordinatorTests: XCTestCase { XCTAssertFalse(presenter.didShowBlockingUpdate) } - func testBlockingUpdateAvailable() async { + func testImmediateDuplicateChecksAreThrottledBeforeFetching() async { // Given - let coordinator = AppUpdateCoordinator( + remoteConfigStore.inAppUpdateFlexibleIntervalInDays = 90 + let firstCoordinator = AppUpdateCoordinator( currentVersion: "24.6", currentOsVersion: "17.0", service: service, presenter: presenter, remoteConfigStore: remoteConfigStore, - store: store, + store: InMemoryUserDefaults(), + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, + isJetpack: true, + isLoggedIn: true, + isInAppUpdatesEnabled: true + ) + let secondCoordinator = AppUpdateCoordinator( + currentVersion: "24.6", + currentOsVersion: "17.0", + service: service, + presenter: presenter, + remoteConfigStore: remoteConfigStore, + store: InMemoryUserDefaults(), + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, isJetpack: true, isLoggedIn: true, isInAppUpdatesEnabled: true ) - remoteConfigStore.jetpackInAppUpdateBlockingVersion = "24.7" // When - await coordinator.checkForAppUpdates() + await firstCoordinator.checkForAppUpdates() + service.lookupCount = 0 + presenter.didShowNotice = false + await secondCoordinator.checkForAppUpdates() // Then - XCTAssertTrue(service.didLookup) + XCTAssertFalse(service.didLookup) XCTAssertFalse(presenter.didShowNotice) - XCTAssertTrue(presenter.didShowBlockingUpdate) + XCTAssertEqual(presenter.showNoticeCount, 1) } - // MARK: - AppUpdatePresenter flexible notice guard - - func testShowNoticePostsExactlyOneFlexibleNotice() throws { + func testDuplicateCheckThrottleAllowsLaterChecks() async { // Given - let dispatcher = ActionDispatcher() - let noticeStore = NoticeStore(dispatcher: dispatcher) - let presenter = AppUpdatePresenter(noticeStore: noticeStore, dispatcher: dispatcher) - let appStoreInfo = try makeAppStoreInfo() + remoteConfigStore.inAppUpdateFlexibleIntervalInDays = 90 + let firstCoordinator = AppUpdateCoordinator( + currentVersion: "24.6", + currentOsVersion: "17.0", + service: service, + presenter: presenter, + remoteConfigStore: remoteConfigStore, + store: InMemoryUserDefaults(), + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, + isJetpack: true, + isLoggedIn: true, + isInAppUpdatesEnabled: true + ) + let secondCoordinator = AppUpdateCoordinator( + currentVersion: "24.6", + currentOsVersion: "17.0", + service: service, + presenter: presenter, + remoteConfigStore: remoteConfigStore, + store: InMemoryUserDefaults(), + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, + isJetpack: true, + isLoggedIn: true, + isInAppUpdatesEnabled: true + ) // When - presenter.showNotice(using: appStoreInfo) + await firstCoordinator.checkForAppUpdates() + currentDateProvider.dateToReturn = currentDateProvider.date().addingTimeInterval(5 * 60) + await secondCoordinator.checkForAppUpdates() // Then - XCTAssertEqual(noticeStore.currentNotice?.tag, AppUpdatePresenter.flexibleUpdateNoticeTag) + XCTAssertEqual(service.lookupCount, 2) + XCTAssertEqual(presenter.showNoticeCount, 2) } - func testShowNoticeSuppressesSecondFlexibleNoticeWhileOneIsShowing() throws { + func testBlockingUpdateAvailable() async { // Given - let dispatcher = ActionDispatcher() - let noticeStore = NoticeStore(dispatcher: dispatcher) - let presenter = AppUpdatePresenter(noticeStore: noticeStore, dispatcher: dispatcher) - let appStoreInfo = try makeAppStoreInfo() - presenter.showNotice(using: appStoreInfo) - let firstNotice = try XCTUnwrap(noticeStore.currentNotice) - - // When a second presentation races in while the first is still showing - presenter.showNotice(using: appStoreInfo) - - // Then the current notice is still the first one and nothing was queued - XCTAssertEqual(noticeStore.currentNotice, firstNotice) - // Dismissing the current notice leaves no queued duplicate behind - ActionDispatcher.dispatch(NoticeAction.dismiss, dispatcher: dispatcher) - XCTAssertNil(noticeStore.currentNotice) - } + let coordinator = AppUpdateCoordinator( + currentVersion: "24.6", + currentOsVersion: "17.0", + service: service, + presenter: presenter, + remoteConfigStore: remoteConfigStore, + store: store, + checkThrottle: checkThrottle, + currentDateProvider: currentDateProvider, + isJetpack: true, + isLoggedIn: true, + isInAppUpdatesEnabled: true + ) + remoteConfigStore.jetpackInAppUpdateBlockingVersion = "24.7" - func testShowNoticeCanPostAgainAfterPreviousNoticeIsCleared() throws { - // Given - let dispatcher = ActionDispatcher() - let noticeStore = NoticeStore(dispatcher: dispatcher) - let presenter = AppUpdatePresenter(noticeStore: noticeStore, dispatcher: dispatcher) - let appStoreInfo = try makeAppStoreInfo() - presenter.showNotice(using: appStoreInfo) - XCTAssertNotNil(noticeStore.currentNotice) - - // When the first notice is cleared and a later legitimate cycle posts again - ActionDispatcher.dispatch(NoticeAction.dismiss, dispatcher: dispatcher) - XCTAssertNil(noticeStore.currentNotice) - presenter.showNotice(using: appStoreInfo) - - // Then the guard does not permanently latch - XCTAssertEqual(noticeStore.currentNotice?.tag, AppUpdatePresenter.flexibleUpdateNoticeTag) - } + // When + await coordinator.checkForAppUpdates() - private func makeAppStoreInfo() throws -> AppStoreLookupResponse.AppStoreInfo { - let data = try Bundle.test.json(named: "app-store-lookup-response") - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - let response = try decoder.decode(AppStoreLookupResponse.self, from: data) - return try XCTUnwrap(response.results.first) + // Then + XCTAssertTrue(service.didLookup) + XCTAssertFalse(presenter.didShowNotice) + XCTAssertTrue(presenter.didShowBlockingUpdate) } } private final class MockAppStoreSearchService: AppStoreSearchProtocol { - var didLookup = false + var lookupCount = 0 + + var didLookup: Bool { + lookupCount > 0 + } - var appID: String { "1234567890" } + var appID: String { + "1234567890" + } func lookup() async throws -> AppStoreLookupResponse { - didLookup = true + lookupCount += 1 return try getMockLookupResponse() } @@ -276,16 +320,18 @@ private final class MockAppUpdatePresenter: AppUpdatePresenterProtocol { var didShowNotice = false var didShowBlockingUpdate = false var didOpenAppStore = false + var showNoticeCount = 0 - func showNotice(using appStoreInfo: AppStoreLookupResponse.AppStoreInfo) { + func showNotice(using _: AppStoreLookupResponse.AppStoreInfo) { + showNoticeCount += 1 didShowNotice = true } - func showBlockingUpdate(using appStoreInfo: AppStoreLookupResponse.AppStoreInfo) { + func showBlockingUpdate(using _: AppStoreLookupResponse.AppStoreInfo) { didShowBlockingUpdate = true } - func openAppStore(appStoreUrl: String) { + func openAppStore(appStoreUrl _: String) { didOpenAppStore = true } } diff --git a/WordPress/Classes/Services/AppUpdate/AppUpdateCoordinator.swift b/WordPress/Classes/Services/AppUpdate/AppUpdateCoordinator.swift index 097bbb7108fd..0e4d90c7e07e 100644 --- a/WordPress/Classes/Services/AppUpdate/AppUpdateCoordinator.swift +++ b/WordPress/Classes/Services/AppUpdate/AppUpdateCoordinator.swift @@ -13,6 +13,8 @@ final class AppUpdateCoordinator { private let presenter: AppUpdatePresenterProtocol private let remoteConfigStore: RemoteConfigStore private let store: UserPersistentRepository + private let checkThrottle: AppUpdateCheckThrottle + private let currentDateProvider: CurrentDateProvider private let isJetpack: Bool private let isLoggedIn: Bool private let isInAppUpdatesEnabled: Bool @@ -25,6 +27,8 @@ final class AppUpdateCoordinator { presenter: AppUpdatePresenterProtocol = AppUpdatePresenter(), remoteConfigStore: RemoteConfigStore = RemoteConfigStore(), store: UserPersistentRepository = UserDefaults.standard, + checkThrottle: AppUpdateCheckThrottle = .shared, + currentDateProvider: CurrentDateProvider = DefaultCurrentDateProvider(), isJetpack: Bool = AppConfiguration.isJetpack, isLoggedIn: Bool = AccountHelper.isLoggedIn, isInAppUpdatesEnabled: Bool = RemoteFeatureFlag.inAppUpdates.enabled(), @@ -36,6 +40,8 @@ final class AppUpdateCoordinator { self.presenter = presenter self.remoteConfigStore = remoteConfigStore self.store = store + self.checkThrottle = checkThrottle + self.currentDateProvider = currentDateProvider self.isJetpack = isJetpack self.isLoggedIn = isLoggedIn self.isInAppUpdatesEnabled = isInAppUpdatesEnabled @@ -50,6 +56,9 @@ final class AppUpdateCoordinator { guard isLoggedIn else { return } + guard checkThrottle.shouldCheck(now: currentDateProvider.date()) else { + return + } guard let updateType = await appUpdateType else { return } @@ -59,7 +68,7 @@ final class AppUpdateCoordinator { presenter.showBlockingUpdate(using: appStoreInfo) } else { presenter.showNotice(using: appStoreInfo) - lastSeenFlexibleUpdateDate = Date.now + lastSeenFlexibleUpdateDate = currentDateProvider.date() } } @@ -108,7 +117,7 @@ final class AppUpdateCoordinator { private func fetchAppStoreInfo() async -> AppStoreLookupResponse.AppStoreInfo? { do { let response = try await service.lookup() - lastFetchedAppStoreInfoDate = Date.now + lastFetchedAppStoreInfoDate = currentDateProvider.date() return response.results.first { $0.trackId == Int(service.appID) } } catch { DDLogError("Error fetching app store info: \(error)") @@ -133,7 +142,7 @@ extension AppUpdateCoordinator { guard let lastFetchedAppStoreInfoDate else { return true } - guard let daysElapsed = Calendar.current.dateComponents([.day], from: lastFetchedAppStoreInfoDate, to: Date.now).day else { + guard let daysElapsed = Calendar.current.dateComponents([.day], from: lastFetchedAppStoreInfoDate, to: currentDateProvider.date()).day else { return false } return daysElapsed >= Constants.lastFetchedAppStoreInfoThresholdInDays @@ -160,13 +169,38 @@ extension AppUpdateCoordinator { guard let lastSeenFlexibleUpdateDate else { return true } - guard let daysElapsed = Calendar.current.dateComponents([.day], from: lastSeenFlexibleUpdateDate, to: Date.now).day else { + guard let daysElapsed = Calendar.current.dateComponents([.day], from: lastSeenFlexibleUpdateDate, to: currentDateProvider.date()).day else { return false } return daysElapsed >= flexibleIntervalInDays } } +final class AppUpdateCheckThrottle: @unchecked Sendable { + static let shared = AppUpdateCheckThrottle() + + private let lock = NSLock() + private let minimumInterval: TimeInterval + private var lastCheckDate: Date? + + init(minimumInterval: TimeInterval = 5 * 60) { + self.minimumInterval = minimumInterval + } + + func shouldCheck(now: Date) -> Bool { + lock.lock() + defer { + lock.unlock() + } + + if let lastCheckDate, now.timeIntervalSince(lastCheckDate) < minimumInterval { + return false + } + lastCheckDate = now + return true + } +} + private enum Constants { static let lastSeenFlexibleUpdateDateKey = "last-seen-flexible-update-date-key" static let lastFetchedAppStoreInfoDateKey = "last-fetched-app-store-info-date-key" diff --git a/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift b/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift index 7ef1f00f7398..11c241191ea2 100644 --- a/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift +++ b/WordPress/Classes/Services/AppUpdate/AppUpdatePresenter.swift @@ -9,26 +9,7 @@ protocol AppUpdatePresenterProtocol { } final class AppUpdatePresenter: AppUpdatePresenterProtocol { - /// Stable tag used to identify the flexible in-app-update notice so a second - /// presentation can be suppressed while one is already showing. - static let flexibleUpdateNoticeTag: Notice.Tag = "in-app-update-flexible" - - private let noticeStore: NoticeStore - private let dispatcher: ActionDispatcher - - init( - noticeStore: NoticeStore = StoreContainer.shared.notice, - dispatcher: ActionDispatcher = .global - ) { - self.noticeStore = noticeStore - self.dispatcher = dispatcher - } - func showNotice(using appStoreInfo: AppStoreLookupResponse.AppStoreInfo) { - guard noticeStore.currentNotice?.tag != Self.flexibleUpdateNoticeTag else { - // Don't post another flexible update notice if one is already showing - return - } let viewModel = AppStoreInfoViewModel(appStoreInfo) let notice = Notice( title: viewModel.title, @@ -36,8 +17,7 @@ final class AppUpdatePresenter: AppUpdatePresenterProtocol { feedbackType: .warning, style: InAppUpdateNoticeStyle(), actionTitle: viewModel.updateButtonTitle, - cancelTitle: viewModel.cancelButtonTitle, - tag: Self.flexibleUpdateNoticeTag + cancelTitle: viewModel.cancelButtonTitle ) { accepted in if accepted { WPAnalytics.track(.inAppUpdateAccepted, properties: ["type": "flexible"]) @@ -46,14 +26,15 @@ final class AppUpdatePresenter: AppUpdatePresenterProtocol { WPAnalytics.track(.inAppUpdateDismissed) } } - ActionDispatcher.dispatch(NoticeAction.post(notice), dispatcher: dispatcher) + ActionDispatcher.dispatch(NoticeAction.post(notice)) WPAnalytics.track(.inAppUpdateShown, properties: ["type": "flexible"]) } func showBlockingUpdate(using appStoreInfo: AppStoreLookupResponse.AppStoreInfo) { guard let window = UIApplication.sharedIfAvailable()?.mainWindow, let topViewController = window.topmostPresentedViewController, - !((topViewController as? UINavigationController)?.viewControllers.first is BlockingUpdateViewController) else { + !((topViewController as? UINavigationController)?.viewControllers.first is BlockingUpdateViewController) + else { // Don't show if the view is already being displayed return }