Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

In-App Updates: Show flexible update again after a specified interval #23221

Merged
merged 9 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
48 changes: 44 additions & 4 deletions WordPress/Classes/Services/AppUpdate/AppUpdateCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ struct AppUpdateType {
}

final class AppUpdateCoordinator {

private let currentVersion: String?
private let currentOsVersion: String
private let service: AppStoreSearchProtocol
private let presenter: AppUpdatePresenterProtocol
private let remoteConfigStore: RemoteConfigStore
private let store: UserPersistentRepository
private let isJetpack: Bool
private let isLoggedIn: Bool
private let isInAppUpdatesEnabled: Bool
Expand All @@ -23,6 +23,7 @@ final class AppUpdateCoordinator {
service: AppStoreSearchProtocol = AppStoreSearchService(),
presenter: AppUpdatePresenterProtocol = AppUpdatePresenter(),
remoteConfigStore: RemoteConfigStore = RemoteConfigStore(),
store: UserPersistentRepository = UserDefaults.standard,
isJetpack: Bool = AppConfiguration.isJetpack,
isLoggedIn: Bool = AccountHelper.isLoggedIn,
isInAppUpdatesEnabled: Bool = RemoteFeatureFlag.inAppUpdates.enabled(),
Expand All @@ -33,6 +34,7 @@ final class AppUpdateCoordinator {
self.service = service
self.presenter = presenter
self.remoteConfigStore = remoteConfigStore
self.store = store
self.isJetpack = isJetpack
self.isLoggedIn = isLoggedIn
self.isInAppUpdatesEnabled = isInAppUpdatesEnabled
Expand All @@ -51,10 +53,12 @@ final class AppUpdateCoordinator {
return
}

let appStoreInfo = updateType.appStoreInfo
if updateType.isRequired {
presenter.showBlockingUpdate(using: updateType.appStoreInfo)
presenter.showBlockingUpdate(using: appStoreInfo)
} else {
presenter.showNotice(using: updateType.appStoreInfo)
presenter.showNotice(using: appStoreInfo)
setLastSeenFlexibleUpdateDate(Date.now, for: appStoreInfo.version)
}
}

Expand All @@ -76,7 +80,7 @@ final class AppUpdateCoordinator {
if let blockingVersion, currentVersion.isLower(than: blockingVersion), blockingVersion.isLowerThanOrEqual(to: appStoreInfo.version) {
return AppUpdateType(appStoreInfo: appStoreInfo, isRequired: true)
}
if currentVersion.isLower(than: appStoreInfo.version) {
if currentVersion.isLower(than: appStoreInfo.version), shouldShowFlexibleUpdate(for: appStoreInfo.version) {
return AppUpdateType(appStoreInfo: appStoreInfo, isRequired: false)
}
return nil
Expand All @@ -101,6 +105,42 @@ final class AppUpdateCoordinator {
}
}

// MARK: - Flexible Interval

extension AppUpdateCoordinator {
private var flexibleIntervalInDays: Int? {
RemoteConfigParameter.inAppUpdateFlexibleIntervalInDays.value(using: remoteConfigStore)
}

private func lastSeenFlexibleUpdateKey(for version: String) -> String {
return "\(version)-\(Constants.lastSeenFlexibleUpdateDateKey)"
}

private func getLastSeenFlexibleUpdateDate(for version: String) -> Date? {
store.object(forKey: lastSeenFlexibleUpdateKey(for: version)) as? Date
}

private func setLastSeenFlexibleUpdateDate(_ date: Date, for version: String) {
store.set(date, forKey: lastSeenFlexibleUpdateKey(for: version))
}

private func shouldShowFlexibleUpdate(for version: String) -> Bool {
guard let flexibleIntervalInDays else {
return false
momo-ozawa marked this conversation as resolved.
Show resolved Hide resolved
}
guard let lastSeenFlexibleUpdateDate = getLastSeenFlexibleUpdateDate(for: version) else {
return true
}
let secondsInDay: TimeInterval = 86_400
let secondsSinceLastSeen = -lastSeenFlexibleUpdateDate.timeIntervalSinceNow
return secondsSinceLastSeen > Double(flexibleIntervalInDays) * secondsInDay
}
}

private enum Constants {
static let lastSeenFlexibleUpdateDateKey = "last-seen-flexible-update-date-key"
}

private extension String {
func isLower(than anotherVersionString: String) -> Bool {
self.compare(anotherVersionString, options: .numeric) == .orderedAscending
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ final class AppUpdatePresenter: AppUpdatePresenterProtocol {
}
ActionDispatcher.dispatch(NoticeAction.post(notice))
WPAnalytics.track(.inAppUpdateShown, properties: ["type": "flexible"])
// Todo: if the notice is dismissed, show notice again after a defined interval
}

func showBlockingUpdate(using appStoreInfo: AppStoreLookupResponse.AppStoreInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ enum RemoteConfigParameter: CaseIterable, RemoteParameter {
case phaseFourOverlayFrequency
case wordPressInAppUpdateBlockingVersion
case jetpackInAppUpdateBlockingVersion
case inAppUpdateFlexibleIntervalInDays

var key: String {
switch self {
Expand Down Expand Up @@ -63,6 +64,8 @@ enum RemoteConfigParameter: CaseIterable, RemoteParameter {
return "wp_in_app_update_blocking_version_ios"
case .jetpackInAppUpdateBlockingVersion:
return "jp_in_app_update_blocking_version_ios"
case .inAppUpdateFlexibleIntervalInDays:
return "in_app_update_flexible_interval_in_days_ios"
}
}

Expand Down Expand Up @@ -92,6 +95,8 @@ enum RemoteConfigParameter: CaseIterable, RemoteParameter {
return nil
case .jetpackInAppUpdateBlockingVersion:
return nil
case .inAppUpdateFlexibleIntervalInDays:
return nil
}
}

Expand Down Expand Up @@ -121,6 +126,8 @@ enum RemoteConfigParameter: CaseIterable, RemoteParameter {
return "WP In-App Update Blocking Version"
case .jetpackInAppUpdateBlockingVersion:
return "JP In-App Update Blocking Version"
case .inAppUpdateFlexibleIntervalInDays:
return "In-App Update Flexible Interval (Days)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ final class AppUpdateCoordinatorTests: XCTestCase {
private let service = MockAppStoreSearchService()
private let presenter = MockAppUpdatePresenter()
private let remoteConfigStore = RemoteConfigStoreMock()
private var store = UserDefaults(suiteName: "app-update-coordinator-tests")!

override func tearDown() {
super.tearDown()
store.removePersistentDomain(forName: "app-update-coordinator-tests")
}

func testInAppUpdatesDisabled() async {
// Given
Expand Down Expand Up @@ -58,6 +64,7 @@ final class AppUpdateCoordinatorTests: XCTestCase {
service: service,
presenter: presenter,
remoteConfigStore: remoteConfigStore,
store: store,
isLoggedIn: false,
isInAppUpdatesEnabled: true,
delayInDays: Int.max
Expand All @@ -80,6 +87,7 @@ final class AppUpdateCoordinatorTests: XCTestCase {
service: service,
presenter: presenter,
remoteConfigStore: remoteConfigStore,
store: store,
isJetpack: true,
isLoggedIn: true,
isInAppUpdatesEnabled: true
Expand All @@ -102,6 +110,7 @@ final class AppUpdateCoordinatorTests: XCTestCase {
service: service,
presenter: presenter,
remoteConfigStore: remoteConfigStore,
store: store,
isJetpack: true,
isLoggedIn: true,
isInAppUpdatesEnabled: true
Expand All @@ -117,18 +126,20 @@ final class AppUpdateCoordinatorTests: XCTestCase {
XCTAssertFalse(presenter.didShowBlockingUpdate)
}

func testFlexibleUpdateAvailable() async {
func testFlexibleUpdateAvailableShownOnceWithinInterval() async {
// Given
let coordinator = AppUpdateCoordinator(
currentVersion: "24.6",
currentOsVersion: "17.0",
service: service,
presenter: presenter,
remoteConfigStore: remoteConfigStore,
store: store,
isJetpack: true,
isLoggedIn: true,
isInAppUpdatesEnabled: true
)
remoteConfigStore.inAppUpdateFlexibleIntervalInDays = 5

// When
await coordinator.checkForAppUpdates()
Expand All @@ -137,6 +148,15 @@ final class AppUpdateCoordinatorTests: XCTestCase {
XCTAssertTrue(service.didLookup)
XCTAssertTrue(presenter.didShowNotice)
XCTAssertFalse(presenter.didShowBlockingUpdate)

// When we check for updates again within the flexible interval
presenter.didShowNotice = false // Reset
await coordinator.checkForAppUpdates()

// Then the flexible notice isn't shown again
XCTAssertTrue(service.didLookup)
XCTAssertFalse(presenter.didShowNotice)
XCTAssertFalse(presenter.didShowBlockingUpdate)
}

func testBlockingUpdateAvailable() async {
Expand Down
4 changes: 4 additions & 0 deletions WordPress/WordPressTest/RemoteConfigStoreMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class RemoteConfigStoreMock: RemoteConfigStore {
var blazeNonDismissibleStep: String?
var blazeFlowCompletedStep: String?
var jetpackInAppUpdateBlockingVersion: String?
var inAppUpdateFlexibleIntervalInDays: Int?

override func value(for key: String) -> Any? {
if key == "phase_three_blog_post" {
Expand All @@ -33,6 +34,9 @@ class RemoteConfigStoreMock: RemoteConfigStore {
if key == "jp_in_app_update_blocking_version_ios" {
return jetpackInAppUpdateBlockingVersion
}
if key == "in_app_update_flexible_interval_in_days_ios" {
return inAppUpdateFlexibleIntervalInDays
}
return super.value(for: key)
}
}