diff --git a/WordPress/Classes/Stores/RemoteConfigStore.swift b/WordPress/Classes/Stores/RemoteConfigStore.swift index c7d183b79d70..22542735bffe 100644 --- a/WordPress/Classes/Stores/RemoteConfigStore.swift +++ b/WordPress/Classes/Stores/RemoteConfigStore.swift @@ -25,6 +25,13 @@ class RemoteConfigStore { // MARK: Public Functions + /// Looks up the value for a remote config parameter. + /// - Parameters: + /// - key: The key associated with a remote config parameter + public func value(for key: String) -> Any? { + return cache[key] + } + /// Fetches remote config values from the server. /// - Parameter callback: An optional callback that can be used to update UI following the fetch. It is not called on the UI thread. public func update(then callback: FetchCallback? = nil) { @@ -49,7 +56,7 @@ extension RemoteConfigStore { typealias FetchCallback = () -> Void /// The local cache stores remote config values between runs so that the most recently fetched set are ready to go as soon as this object is instantiated. - private(set) var cache: [String: Any] { + private var cache: [String: Any] { get { // Read from the cache in a thread-safe way queue.sync { diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteConfigParameter.swift b/WordPress/Classes/Utility/BuildInformation/RemoteConfigParameter.swift index 6d80e4d123e4..4d114b253695 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteConfigParameter.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteConfigParameter.swift @@ -12,7 +12,7 @@ struct RemoteConfigParameter { private let store: RemoteConfigStore private var serverValue: T? { - return store.cache[key] as? T + return store.value(for: key) as? T } // MARK: Initializer diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m index 95624eb5ad41..65f0e60f164b 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m @@ -615,6 +615,7 @@ - (void)setRestorableSelectedIndexPath:(NSIndexPath *)restorableSelectedIndexPat switch (section.category) { case BlogDetailsSectionCategoryQuickAction: case BlogDetailsSectionCategoryQuickStart: + case BlogDetailsSectionCategoryJetpackBrandingCard: case BlogDetailsSectionCategoryDomainCredit: { _restorableSelectedIndexPath = nil; } @@ -700,6 +701,7 @@ - (void)reloadTableViewPreservingSelection switch (section.category) { case BlogDetailsSectionCategoryQuickAction: case BlogDetailsSectionCategoryQuickStart: + case BlogDetailsSectionCategoryJetpackBrandingCard: case BlogDetailsSectionCategoryDomainCredit: { BlogDetailsSubsection subsection = [self shouldShowDashboard] ? BlogDetailsSubsectionHome : BlogDetailsSubsectionStats; BlogDetailsSectionCategory category = [self sectionCategoryWithSubsection:subsection blog: self.blog]; @@ -742,7 +744,7 @@ - (void)configureTableViewData if (MigrationSuccessCardView.shouldShowMigrationSuccessCard == YES) { [marr addObject:[self migrationSuccessSectionViewModel]]; } - if (JetpackBrandingMenuCardCoordinator.shouldShowCard == YES) { + if (self.shouldShowJetpackBrandingMenuCard == YES) { [marr addObject:[self jetpackCardSectionViewModel]]; } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayGeneralViewModel.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayGeneralViewModel.swift index c57d8950ddf5..f63bdd55aeba 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayGeneralViewModel.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayGeneralViewModel.swift @@ -70,6 +70,8 @@ struct JetpackFullscreenOverlayGeneralViewModel: JetpackFullscreenOverlayViewMod return Strings.PhaseTwoAndThree.notificationsTitle case (.three, .reader): return Strings.PhaseTwoAndThree.readerTitle + case (.three, _): + return Strings.PhaseThree.generalTitle default: return "" } @@ -110,7 +112,7 @@ struct JetpackFullscreenOverlayGeneralViewModel: JetpackFullscreenOverlayViewMod case .login: fallthrough case .appOpen: - return "" // TODO: Add new animation when ready + return Constants.allFeaturesLogosAnimationLtr } } @@ -127,7 +129,7 @@ struct JetpackFullscreenOverlayGeneralViewModel: JetpackFullscreenOverlayViewMod case .login: fallthrough case .appOpen: - return "" // TODO: Add new animation when ready + return Constants.allFeaturesLogosAnimationRtl } } @@ -175,13 +177,15 @@ struct JetpackFullscreenOverlayGeneralViewModel: JetpackFullscreenOverlayViewMod } var continueButtonText: String? { - switch source { - case .stats: + switch (source, phase) { + case (.stats, _): return Strings.General.statsContinueButtonTitle - case .notifications: + case (.notifications, _): return Strings.General.notificationsContinueButtonTitle - case .reader: + case (.reader, _): return Strings.General.readerContinueButtonTitle + case (_, .three): + return Strings.PhaseThree.generalContinueButtonTitle default: return nil } @@ -242,6 +246,8 @@ private extension JetpackFullscreenOverlayGeneralViewModel { static let readerLogoAnimationRtl = "JetpackReaderLogoAnimation_rtl" static let notificationsLogoAnimationLtr = "JetpackNotificationsLogoAnimation_ltr" static let notificationsLogoAnimationRtl = "JetpackNotificationsLogoAnimation_rtl" + static let allFeaturesLogosAnimationLtr = "JetpackAllFeaturesLogosAnimation_ltr" + static let allFeaturesLogosAnimationRtl = "JetpackAllFeaturesLogosAnimation_rtl" } enum Strings { @@ -314,5 +320,15 @@ private extension JetpackFullscreenOverlayGeneralViewModel { value: "Switching is free and only takes a minute.", comment: "A footnote in a screen displayed when the user accesses a Jetpack powered feature from the WordPress app. The screen showcases the Jetpack app.") } + + enum PhaseThree { + static let generalTitle = NSLocalizedString("jetpack.fullscreen.overlay.phaseThree.general.title", + value: "Jetpack features are moving soon.", + comment: "Title of a screen that showcases the Jetpack app.") + + static let generalContinueButtonTitle = NSLocalizedString("jetpack.fullscreen.overlay.phaseThree.general.continue.title", + value: "Continue without Jetpack", + comment: "Title of a button that dismisses an overlay that showcases the Jetpack app.") + } } } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift index 3d25446ab765..7ba3fce59236 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift @@ -2,9 +2,16 @@ import Foundation extension BlogDetailsViewController { + @objc var shouldShowJetpackBrandingMenuCard: Bool { + let presenter = JetpackBrandingMenuCardPresenter() + return presenter.shouldShowCard() + } + @objc func jetpackCardSectionViewModel() -> BlogDetailsSection { let row = BlogDetailsRow() - row.callback = {} + row.callback = { + JetpackFeaturesRemovalCoordinator.presentOverlayIfNeeded(from: .card, in: self) + } let section = BlogDetailsSection(title: nil, rows: [row], @@ -12,4 +19,9 @@ extension BlogDetailsViewController { category: .jetpackBrandingCard) return section } + + func reloadTableView() { + configureTableViewData() + reloadTableViewPreservingSelection() + } } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift index e221834e9d6e..25ea813e53e6 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift @@ -5,7 +5,8 @@ class JetpackBrandingMenuCardCell: UITableViewCell { // MARK: Private Variables - private weak var viewController: UIViewController? + private weak var viewController: BlogDetailsViewController? + private var presenter: JetpackBrandingMenuCardPresenter /// Sets the animation based on the language orientation private var animation: Animation? { @@ -119,11 +120,13 @@ class JetpackBrandingMenuCardCell: UITableViewCell { // MARK: Initializers override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + presenter = JetpackBrandingMenuCardPresenter() super.init(style: style, reuseIdentifier: reuseIdentifier) commonInit() } required init?(coder: NSCoder) { + presenter = JetpackBrandingMenuCardPresenter() super.init(coder: coder) commonInit() } @@ -144,7 +147,7 @@ class JetpackBrandingMenuCardCell: UITableViewCell { private func setupContent() { logosAnimationView.play() - let config = JetpackBrandingMenuCardCoordinator.cardConfig + let config = presenter.cardConfig() descriptionLabel.text = config?.description learnMoreSuperview.isHidden = config?.learnMoreButtonURL == nil } @@ -152,7 +155,7 @@ class JetpackBrandingMenuCardCell: UITableViewCell { // MARK: Actions @objc private func learnMoreButtonTapped() { - guard let config = JetpackBrandingMenuCardCoordinator.cardConfig, + guard let config = presenter.cardConfig(), let urlString = config.learnMoreButtonURL, let url = URL(string: urlString) else { return @@ -186,11 +189,15 @@ private extension JetpackBrandingMenuCardCell { // MARK: Actions private func remindMeLaterTapped() { - // TODO: Implement this + presenter.remindLaterTapped() + viewController?.reloadTableView() + // TODO: Track button tapped } private func hideThisTapped() { - // TODO: Implement this + presenter.hideThisTapped() + viewController?.reloadTableView() + // TODO: Track button tapped } } @@ -276,7 +283,7 @@ private extension JetpackBrandingMenuCardCell { extension JetpackBrandingMenuCardCell { @objc(configureWithViewController:) - func configure(with viewController: UIViewController) { + func configure(with viewController: BlogDetailsViewController) { self.viewController = viewController } } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCoordinator.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCoordinator.swift deleted file mode 100644 index 3428b88ec7fc..000000000000 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCoordinator.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation - -@objc -class JetpackBrandingMenuCardCoordinator: NSObject { - - struct Config { - let description: String - let learnMoreButtonURL: String? - } - - static var cardConfig: Config? { - let phase = JetpackFeaturesRemovalCoordinator.generalPhase() - switch phase { - case .three: - let description = Strings.phaseThreeDescription - let url = RemoteConfig().phaseThreeBlogPostUrl.value - return .init(description: description, learnMoreButtonURL: url) - default: - return nil - } - } - - @objc static var shouldShowCard: Bool { - return cardConfig != nil - } -} - -private extension JetpackBrandingMenuCardCoordinator { - enum Strings { - static let phaseThreeDescription = NSLocalizedString("jetpack.menuCard.description", - value: "Stats, Reader, Notifications and other features will soon move to the Jetpack mobile app.", - comment: "Description inside a menu card communicating that features are moving to the Jetpack app.") - } -} diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardPresenter.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardPresenter.swift new file mode 100644 index 000000000000..230f9b46cb41 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardPresenter.swift @@ -0,0 +1,100 @@ +import Foundation + +class JetpackBrandingMenuCardPresenter { + + struct Config { + let description: String + let learnMoreButtonURL: String? + } + + // MARK: Private Variables + + private let remoteConfigStore: RemoteConfigStore + private let persistenceStore: UserPersistentRepository + private let featureFlagStore: RemoteFeatureFlagStore + private let currentDateProvider: CurrentDateProvider + + // MARK: Initializers + + init(remoteConfigStore: RemoteConfigStore = RemoteConfigStore(), + featureFlagStore: RemoteFeatureFlagStore = RemoteFeatureFlagStore(), + persistenceStore: UserPersistentRepository = UserDefaults.standard, + currentDateProvider: CurrentDateProvider = DefaultCurrentDateProvider()) { + self.remoteConfigStore = remoteConfigStore + self.featureFlagStore = featureFlagStore + self.persistenceStore = persistenceStore + self.currentDateProvider = currentDateProvider + } + + // MARK: Public Functions + + func cardConfig() -> Config? { + let phase = JetpackFeaturesRemovalCoordinator.generalPhase(featureFlagStore: featureFlagStore) + switch phase { + case .three: + let description = Strings.phaseThreeDescription + let url = RemoteConfig(store: remoteConfigStore).phaseThreeBlogPostUrl.value + return .init(description: description, learnMoreButtonURL: url) + default: + return nil + } + } + + func shouldShowCard() -> Bool { + let showCardOnDate = showCardOnDate ?? .distantPast // If not set, then return distant past so that the condition below always succeeds + guard shouldHideCard == false, // Card not hidden + showCardOnDate < currentDateProvider.date(), // Interval has passed if temporarily hidden + let _ = cardConfig() else { // Card is enabled in the current phase + return false + } + return true + } + + func remindLaterTapped() { + let now = currentDateProvider.date() + let duration = Constants.remindLaterDurationInDays * Constants.secondsInDay + let newDate = now.addingTimeInterval(TimeInterval(duration)) + showCardOnDate = newDate + } + + func hideThisTapped() { + shouldHideCard = true + } +} + +private extension JetpackBrandingMenuCardPresenter { + var shouldHideCard: Bool { + get { + persistenceStore.bool(forKey: Constants.shouldHideCardKey) + } + + set { + persistenceStore.set(newValue, forKey: Constants.shouldHideCardKey) + } + } + + var showCardOnDate: Date? { + get { + persistenceStore.object(forKey: Constants.showCardOnDateKey) as? Date + } + + set { + persistenceStore.set(newValue, forKey: Constants.showCardOnDateKey) + } + } +} + +private extension JetpackBrandingMenuCardPresenter { + enum Constants { + static let secondsInDay = 86_400 + static let remindLaterDurationInDays = 4 + static let shouldHideCardKey = "JetpackBrandingShouldHideCardKey" + static let showCardOnDateKey = "JetpackBrandingShowCardOnDateKey" + } + + enum Strings { + static let phaseThreeDescription = NSLocalizedString("jetpack.menuCard.description", + value: "Stats, Reader, Notifications and other features will move to the Jetpack mobile app soon.", + comment: "Description inside a menu card communicating that features are moving to the Jetpack app.") + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 65b7c025b294..e0ae137d4a65 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1577,11 +1577,12 @@ 80535DB82946C79700873161 /* JetpackBrandingMenuCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DB72946C79700873161 /* JetpackBrandingMenuCardCell.swift */; }; 80535DBB294ABBF000873161 /* JetpackAllFeaturesLogosAnimation_rtl.json in Resources */ = {isa = PBXBuildFile; fileRef = 80535DB9294ABBEF00873161 /* JetpackAllFeaturesLogosAnimation_rtl.json */; }; 80535DBC294ABBF000873161 /* JetpackAllFeaturesLogosAnimation_ltr.json in Resources */ = {isa = PBXBuildFile; fileRef = 80535DBA294ABBEF00873161 /* JetpackAllFeaturesLogosAnimation_ltr.json */; }; - 80535DBE294AC89200873161 /* JetpackBrandingMenuCardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DBD294AC89200873161 /* JetpackBrandingMenuCardCoordinator.swift */; }; + 80535DBE294AC89200873161 /* JetpackBrandingMenuCardPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DBD294AC89200873161 /* JetpackBrandingMenuCardPresenter.swift */; }; 80535DC0294B7D3200873161 /* BlogDetailsViewController+JetpackBrandingMenuCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DBF294B7D3200873161 /* BlogDetailsViewController+JetpackBrandingMenuCard.swift */; }; 80535DC1294BDE1900873161 /* JetpackBrandingMenuCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DB72946C79700873161 /* JetpackBrandingMenuCardCell.swift */; }; - 80535DC2294BDE2500873161 /* JetpackBrandingMenuCardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DBD294AC89200873161 /* JetpackBrandingMenuCardCoordinator.swift */; }; + 80535DC2294BDE2500873161 /* JetpackBrandingMenuCardPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DBD294AC89200873161 /* JetpackBrandingMenuCardPresenter.swift */; }; 80535DC3294BDE2B00873161 /* BlogDetailsViewController+JetpackBrandingMenuCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DBF294B7D3200873161 /* BlogDetailsViewController+JetpackBrandingMenuCard.swift */; }; + 80535DC5294BF4BE00873161 /* JetpackBrandingMenuCardPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80535DC4294BF4BE00873161 /* JetpackBrandingMenuCardPresenterTests.swift */; }; 8058730B28F7B70B00340C11 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8058730D28F7B70B00340C11 /* InfoPlist.strings */; }; 8067340A27E3A50900ABC95E /* UIViewController+RemoveQuickStart.m in Sources */ = {isa = PBXBuildFile; fileRef = 8067340927E3A50900ABC95E /* UIViewController+RemoveQuickStart.m */; }; 8067340B27E3A50900ABC95E /* UIViewController+RemoveQuickStart.m in Sources */ = {isa = PBXBuildFile; fileRef = 8067340927E3A50900ABC95E /* UIViewController+RemoveQuickStart.m */; }; @@ -6890,8 +6891,9 @@ 80535DB72946C79700873161 /* JetpackBrandingMenuCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBrandingMenuCardCell.swift; sourceTree = ""; }; 80535DB9294ABBEF00873161 /* JetpackAllFeaturesLogosAnimation_rtl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JetpackAllFeaturesLogosAnimation_rtl.json; sourceTree = ""; }; 80535DBA294ABBEF00873161 /* JetpackAllFeaturesLogosAnimation_ltr.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JetpackAllFeaturesLogosAnimation_ltr.json; sourceTree = ""; }; - 80535DBD294AC89200873161 /* JetpackBrandingMenuCardCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBrandingMenuCardCoordinator.swift; sourceTree = ""; }; + 80535DBD294AC89200873161 /* JetpackBrandingMenuCardPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBrandingMenuCardPresenter.swift; sourceTree = ""; }; 80535DBF294B7D3200873161 /* BlogDetailsViewController+JetpackBrandingMenuCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BlogDetailsViewController+JetpackBrandingMenuCard.swift"; sourceTree = ""; }; + 80535DC4294BF4BE00873161 /* JetpackBrandingMenuCardPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackBrandingMenuCardPresenterTests.swift; sourceTree = ""; }; 8058730C28F7B70B00340C11 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 8067340827E3A50900ABC95E /* UIViewController+RemoveQuickStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+RemoveQuickStart.h"; sourceTree = ""; }; 8067340927E3A50900ABC95E /* UIViewController+RemoveQuickStart.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+RemoveQuickStart.m"; sourceTree = ""; }; @@ -12510,6 +12512,7 @@ 8332DD2629259B9700802F7D /* Utility */, 803DE81E290636A4007D4E9C /* JetpackFeaturesRemovalCoordinatorTests.swift */, 801D951C291ADB7E0051993E /* JetpackOverlayFrequencyTrackerTests.swift */, + 80535DC4294BF4BE00873161 /* JetpackBrandingMenuCardPresenterTests.swift */, ); name = Jetpack; sourceTree = ""; @@ -12518,7 +12521,7 @@ isa = PBXGroup; children = ( 80535DB72946C79700873161 /* JetpackBrandingMenuCardCell.swift */, - 80535DBD294AC89200873161 /* JetpackBrandingMenuCardCoordinator.swift */, + 80535DBD294AC89200873161 /* JetpackBrandingMenuCardPresenter.swift */, 80535DBF294B7D3200873161 /* BlogDetailsViewController+JetpackBrandingMenuCard.swift */, ); path = "Menu Card"; @@ -21200,7 +21203,7 @@ 171096CB270F01EA001BCDD6 /* DomainSuggestionsTableViewController.swift in Sources */, 469CE07124BCFB04003BDC8B /* CollapsableHeaderCollectionViewCell.swift in Sources */, 7EB5824720EC41B200002702 /* NotificationContentFactory.swift in Sources */, - 80535DBE294AC89200873161 /* JetpackBrandingMenuCardCoordinator.swift in Sources */, + 80535DBE294AC89200873161 /* JetpackBrandingMenuCardPresenter.swift in Sources */, F53FF3AA23EA725C001AD596 /* SiteIconView.swift in Sources */, FA3536F525B01A2C0005A3A0 /* JetpackRestoreCompleteViewController.swift in Sources */, 7E7947AD210BAC7B005BB851 /* FormattableNoticonRange.swift in Sources */, @@ -22524,6 +22527,7 @@ 80EF9284280CFEB60064A971 /* DashboardPostsSyncManagerTests.swift in Sources */, 7EC9FE0B22C627DB00C5A888 /* PostEditorAnalyticsSessionTests.swift in Sources */, D88A64A0208D8B7D008AE9BC /* StockPhotosMediaGroupTests.swift in Sources */, + 80535DC5294BF4BE00873161 /* JetpackBrandingMenuCardPresenterTests.swift in Sources */, 7EAA66EF22CB36FD00869038 /* TestAnalyticsTracker.swift in Sources */, 931D26F619ED7F7000114F17 /* BlogServiceTest.m in Sources */, B5882C471D5297D1008E0EAA /* NotificationTests.swift in Sources */, @@ -23013,7 +23017,7 @@ 98622EA0274C59A400061A5F /* ReaderDetailCommentsTableViewDelegate.swift in Sources */, FABB21EA2602FC2C00C8785C /* RestoreWarningView.swift in Sources */, 17039225282E6D2800F602E9 /* ViewsVisitorsLineChartCell.swift in Sources */, - 80535DC2294BDE2500873161 /* JetpackBrandingMenuCardCoordinator.swift in Sources */, + 80535DC2294BDE2500873161 /* JetpackBrandingMenuCardPresenter.swift in Sources */, C7D30C652638B07A00A1695B /* JetpackPrologueStyleGuide.swift in Sources */, FABB21EB2602FC2C00C8785C /* GutenbergWebNavigationViewController.swift in Sources */, F4F9D5EC29096CF500502576 /* MigrationHeaderView.swift in Sources */, diff --git a/WordPress/WordPressTest/JetpackBrandingMenuCardPresenterTests.swift b/WordPress/WordPressTest/JetpackBrandingMenuCardPresenterTests.swift new file mode 100644 index 000000000000..9c54b20b52ed --- /dev/null +++ b/WordPress/WordPressTest/JetpackBrandingMenuCardPresenterTests.swift @@ -0,0 +1,164 @@ +import XCTest +@testable import WordPress + +final class JetpackBrandingMenuCardPresenterTests: XCTestCase { + + private var mockUserDefaults: InMemoryUserDefaults! + private var remoteFeatureFlagsStore = RemoteFeatureFlagStoreMock() + private var remoteConfigStore = RemoteConfigStoreMock() + private var currentDateProvider: MockCurrentDateProvider! + + override func setUp() { + mockUserDefaults = InMemoryUserDefaults() + currentDateProvider = MockCurrentDateProvider() + } + + func testShouldShowCardBasedOnPhase() { + // Given + let presenter = JetpackBrandingMenuCardPresenter( + featureFlagStore: remoteFeatureFlagsStore, + persistenceStore: mockUserDefaults) + + // Normal phase + XCTAssertFalse(presenter.shouldShowCard()) + + // Phase One + remoteFeatureFlagsStore.removalPhaseOne = true + XCTAssertFalse(presenter.shouldShowCard()) + + // Phase Two + remoteFeatureFlagsStore.removalPhaseTwo = true + XCTAssertFalse(presenter.shouldShowCard()) + + // Phase Three + remoteFeatureFlagsStore.removalPhaseThree = true + XCTAssertTrue(presenter.shouldShowCard()) + + // Phase Four + remoteFeatureFlagsStore.removalPhaseFour = true + XCTAssertFalse(presenter.shouldShowCard()) + + // Phase New Users + remoteFeatureFlagsStore.removalPhaseNewUsers = true + XCTAssertFalse(presenter.shouldShowCard()) + } + + func testPhaseThreeCardConfig() throws { + // Given + let presenter = JetpackBrandingMenuCardPresenter( + remoteConfigStore: remoteConfigStore, + featureFlagStore: remoteFeatureFlagsStore, + persistenceStore: mockUserDefaults) + remoteFeatureFlagsStore.removalPhaseThree = true + remoteConfigStore.phaseThreeBlogPostUrl = "example.com" + + // When + let config = try XCTUnwrap(presenter.cardConfig()) + + // Then + XCTAssertEqual(config.description, "Stats, Reader, Notifications and other features will move to the Jetpack mobile app soon.") + XCTAssertEqual(config.learnMoreButtonURL, "example.com") + } + + func testHidingTheMenuCard() { + // Given + let presenter = JetpackBrandingMenuCardPresenter( + featureFlagStore: remoteFeatureFlagsStore, + persistenceStore: mockUserDefaults) + remoteFeatureFlagsStore.removalPhaseThree = true + + // When + presenter.hideThisTapped() + + // Then + XCTAssertFalse(presenter.shouldShowCard()) + } + + func testRemindMeLaterTappedRecently() { + // Given + let secondsInDay = TimeInterval(86_400) + let currentDate = Date() + let presenter = JetpackBrandingMenuCardPresenter( + featureFlagStore: remoteFeatureFlagsStore, + persistenceStore: mockUserDefaults, + currentDateProvider: currentDateProvider) + remoteFeatureFlagsStore.removalPhaseThree = true + currentDateProvider.dateToReturn = currentDate + + // When + presenter.remindLaterTapped() + currentDateProvider.dateToReturn = currentDate.addingTimeInterval(secondsInDay) + + // Then + XCTAssertFalse(presenter.shouldShowCard()) + } + + func testRemindMeLaterTappedAndIntervalPassed() { + // Given + let secondsInSevenDays = TimeInterval(86_400 * 4) + let currentDate = Date() + let presenter = JetpackBrandingMenuCardPresenter( + featureFlagStore: remoteFeatureFlagsStore, + persistenceStore: mockUserDefaults, + currentDateProvider: currentDateProvider) + remoteFeatureFlagsStore.removalPhaseThree = true + currentDateProvider.dateToReturn = currentDate + + // When + presenter.remindLaterTapped() + currentDateProvider.dateToReturn = currentDate.addingTimeInterval(secondsInSevenDays + 1) + + // Then + XCTAssertTrue(presenter.shouldShowCard()) + } + +} + +private class RemoteFeatureFlagStoreMock: RemoteFeatureFlagStore { + + var removalPhaseOne = false + var removalPhaseTwo = false + var removalPhaseThree = false + var removalPhaseFour = false + var removalPhaseNewUsers = false + + override func value(for flag: OverrideableFlag) -> Bool { + guard let flag = flag as? WordPress.FeatureFlag else { + return false + } + switch flag { + case .jetpackFeaturesRemovalPhaseOne: + return removalPhaseOne + case .jetpackFeaturesRemovalPhaseTwo: + return removalPhaseTwo + case .jetpackFeaturesRemovalPhaseThree: + return removalPhaseThree + case .jetpackFeaturesRemovalPhaseFour: + return removalPhaseFour + case .jetpackFeaturesRemovalPhaseNewUsers: + return removalPhaseNewUsers + default: + return super.value(for: flag) + } + } +} + +private class RemoteConfigStoreMock: RemoteConfigStore { + + var phaseThreeBlogPostUrl: String? + + override func value(for key: String) -> Any? { + if key == "phase-three-blog-post" { + return phaseThreeBlogPostUrl + } + return super.value(for: key) + } +} + +private class MockCurrentDateProvider: CurrentDateProvider { + var dateToReturn: Date? + + func date() -> Date { + return dateToReturn ?? Date() + } +}