diff --git a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift index 50fde1f95a..a270c9800b 100644 --- a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift +++ b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift @@ -370,6 +370,10 @@ public final class ProjectPageViewController: UIViewController, MessageBannerVie self.viewModel.outputs.goToDashboard .observeForControllerAction() .observeValues { [weak self] param in + guard featureCreatorDashboardEnabled() else { + return + } + self?.goToDashboard(param: param) } diff --git a/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift b/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift index 4315266839..9c4c08cd04 100644 --- a/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift +++ b/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift @@ -167,6 +167,10 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi } public func switchToDashboard(project param: Param?) { + guard featureCreatorDashboardEnabled() else { + return + } + self.viewModel.inputs.switchToDashboard(project: param) } @@ -213,6 +217,10 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi } public func switchToCreatorMessageThread(projectId: Param, messageThread: MessageThread) { + guard featureCreatorDashboardEnabled() else { + return + } + self.switchToDashboard(project: nil) guard @@ -226,6 +234,10 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi } public func switchToProjectActivities(projectId: Param) { + guard featureCreatorDashboardEnabled() else { + return + } + self.switchToDashboard(project: nil) guard @@ -248,44 +260,68 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi case let .search(index): _ = tabBarItem(atIndex: index) ?|> searchTabBarItemStyle case let .dashboard(index): - _ = tabBarItem(atIndex: index) ?|> dashboardTabBarItemStyle + let featureFlaggedTabBarItemStyle = self + .isDashboardViewControllerDisplayable() ? dashboardTabBarItemStyle : + profileTabBarItemStyle(isLoggedIn: data.isLoggedIn, isMember: data.isMember) + _ = tabBarItem(atIndex: index) ?|> featureFlaggedTabBarItemStyle case let .profile(avatarUrl, index): _ = tabBarItem(atIndex: index) ?|> profileTabBarItemStyle(isLoggedIn: data.isLoggedIn, isMember: data.isMember) - guard - data.isLoggedIn == true, - let avatarUrl = avatarUrl, - let dir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first - else { return } - - let hash = avatarUrl.absoluteString.hashValue - let imagePath = "\(dir)/tabbar-avatar-image-\(hash).dat" - let imageUrl = URL(fileURLWithPath: imagePath) - - if let imageData = try? Data(contentsOf: imageUrl) { - let (defaultImage, selectedImage) = tabbarAvatarImageFromData(imageData) - _ = self.tabBarItem(atIndex: index) - ?|> profileTabBarItemStyle(isLoggedIn: true, isMember: data.isMember) - ?|> UITabBarItem.lens.image .~ defaultImage - ?|> UITabBarItem.lens.selectedImage .~ selectedImage - } else { - let sessionConfig = URLSessionConfiguration.default - let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: .main) - let dataTask = session.dataTask(with: avatarUrl) { [weak self] avatarData, _, _ in - guard let avatarData = avatarData else { return } - try? avatarData.write(to: imageUrl, options: [.atomic]) - - let (defaultImage, selectedImage) = tabbarAvatarImageFromData(avatarData) - _ = self?.tabBarItem(atIndex: index) - ?|> profileTabBarItemStyle(isLoggedIn: true, isMember: data.isMember) - ?|> UITabBarItem.lens.image .~ defaultImage - ?|> UITabBarItem.lens.selectedImage .~ selectedImage - } - dataTask.resume() - } + setProfileImage(with: data, avatarUrl: avatarUrl, index: index) + } + } + } + + fileprivate func setProfileImage(with data: TabBarItemsData, avatarUrl: URL?, index: Int) { + guard + data.isLoggedIn == true, + let avatarUrl = avatarUrl, + let dir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first + else { return } + + let hash = avatarUrl.absoluteString.hashValue + let imagePath = "\(dir)/tabbar-avatar-image-\(hash).dat" + let imageUrl = URL(fileURLWithPath: imagePath) + + if let imageData = try? Data(contentsOf: imageUrl) { + let (defaultImage, selectedImage) = tabbarAvatarImageFromData(imageData) + _ = self.tabBarItem(atIndex: index) + ?|> profileTabBarItemStyle(isLoggedIn: true, isMember: data.isMember) + ?|> UITabBarItem.lens.image .~ defaultImage + ?|> UITabBarItem.lens.selectedImage .~ selectedImage + } else { + let sessionConfig = URLSessionConfiguration.default + let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: .main) + let dataTask = session.dataTask(with: avatarUrl) { [weak self] avatarData, _, _ in + guard let avatarData = avatarData else { return } + try? avatarData.write(to: imageUrl, options: [.atomic]) + + let (defaultImage, selectedImage) = tabbarAvatarImageFromData(avatarData) + _ = self?.tabBarItem(atIndex: index) + ?|> profileTabBarItemStyle(isLoggedIn: true, isMember: data.isMember) + ?|> UITabBarItem.lens.image .~ defaultImage + ?|> UITabBarItem.lens.selectedImage .~ selectedImage } + dataTask.resume() + } + } + + fileprivate func isDashboardViewControllerDisplayable() -> Bool { + guard let navigationControllers = self.viewControllers as? [UINavigationController] else { + return false } + + var foundDashboardViewController = false + + for navController in navigationControllers { + if let dashboardVC = navController.viewControllers.first as? DashboardViewController { + foundDashboardViewController = true + break + } + } + + return foundDashboardViewController } fileprivate func tabBarItem(atIndex index: Int) -> UITabBarItem? { diff --git a/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift b/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift index ff473d0e55..72cfca668e 100644 --- a/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift +++ b/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift @@ -12,25 +12,23 @@ final class RemoteConfigFeatureHelpersTests: TestCase { } } - /** FIXME: RemoteConfigValue is not initializing because its' OBJC intiliazer is not available - func testConsentManagementDialog_RemoteConfig_FeatureFlag_True() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [RemoteConfigFeature.consentManagementDialogEnabled.rawValue: true] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - XCTAssertTrue(featureConsentManagementDialogEnabled()) - } - } - - func testFacebookDeprecation_RemoteConfig_FeatureFlag_True() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [RemoteConfigFeature.facebookLoginInterstitialEnabled.rawValue: true] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - XCTAssertTrue(featureFacebookLoginInterstitialEnabled()) - } - } - */ + func testConsentManagementDialog_RemoteConfig_FeatureFlag_True() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [RemoteConfigFeature.consentManagementDialogEnabled.rawValue: true] + + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { + XCTAssertTrue(featureConsentManagementDialogEnabled()) + } + } + + func testFacebookDeprecation_RemoteConfig_FeatureFlag_True() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [RemoteConfigFeature.facebookLoginInterstitialEnabled.rawValue: true] + + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { + XCTAssertTrue(featureFacebookLoginInterstitialEnabled()) + } + } func testFacebookDeprecation_RemoteConfig_FeatureFlag_False() { let mockRemoteConfigClient = MockRemoteConfigClient() diff --git a/Library/ViewModels/RootViewModel.swift b/Library/ViewModels/RootViewModel.swift index 62b6c977ba..ee9a13bc06 100644 --- a/Library/ViewModels/RootViewModel.swift +++ b/Library/ViewModels/RootViewModel.swift @@ -488,21 +488,30 @@ private func generateStandardViewControllers() -> [RootViewControllerData] { private func generatePersonalizedViewControllers(userState: (isMember: Bool, isLoggedIn: Bool)) -> [RootViewControllerData] { - return [.dashboard(isMember: userState.isMember), .profile(isLoggedIn: userState.isLoggedIn)] + if featureCreatorDashboardEnabled() { + return [.dashboard(isMember: userState.isMember), .profile(isLoggedIn: userState.isLoggedIn)] + } + + return [.profile(isLoggedIn: userState.isLoggedIn)] } private func tabData(forUser user: User?) -> TabBarItemsData { let isMember = (user?.stats.memberProjectsCount ?? 0) > 0 - let items: [TabBarItem] = isMember - ? [ - .home(index: 0), .activity(index: 1), .search(index: 2), .dashboard(index: 3), - .profile(avatarUrl: (user?.avatar.small).flatMap(URL.init(string:)), index: 4) - ] - : [ + let items: [TabBarItem] + + switch isMember { + case false: + items = [ .home(index: 0), .activity(index: 1), .search(index: 2), .profile(avatarUrl: (user?.avatar.small).flatMap(URL.init(string:)), index: 3) ] + case true: + items = [ + .home(index: 0), .activity(index: 1), .search(index: 2), .dashboard(index: 3), + .profile(avatarUrl: (user?.avatar.small).flatMap(URL.init(string:)), index: 4) + ] + } return TabBarItemsData( items: items, diff --git a/Library/ViewModels/RootViewModelTests.swift b/Library/ViewModels/RootViewModelTests.swift index 66c088b198..9c6724dac4 100644 --- a/Library/ViewModels/RootViewModelTests.swift +++ b/Library/ViewModels/RootViewModelTests.swift @@ -360,62 +360,129 @@ final class RootViewModelTests: TestCase { } } - func testSetViewControllers() { + func testSetViewControllers_WhenCreatorDashboardEnabled() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [ + RemoteConfigFeature.creatorDashboardEnabled.rawValue: true + ] let viewControllerNames = TestObserver<[String], Never>() - vm.outputs.setViewControllers.map(extractRootNames) - .observe(viewControllerNames.observer) - self.vm.inputs.viewDidLoad() + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { + vm.outputs.setViewControllers.map(extractRootNames) + .observe(viewControllerNames.observer) - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"] - ], - "Show the logged out tabs." - ) + self.vm.inputs.viewDidLoad() - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - self.vm.inputs.userSessionStarted() + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"] + ], + "Show the logged out tabs." + ) - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"] - ], - "Show the logged in tabs." - ) + AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) + self.vm.inputs.userSessionStarted() - AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) - self.vm.inputs.currentUserUpdated() + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"] + ], + "Show the logged in tabs." + ) - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "Dashboard", "BackerDashboard"] - ], - "Show the creator dashboard tab." - ) + AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) + self.vm.inputs.currentUserUpdated() - AppEnvironment.logout() - self.vm.inputs.userSessionEnded() + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"], + ["Discovery", "Activities", "Search", "Dashboard", "BackerDashboard"] + ], + "Show the creator dashboard tab." + ) - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "Dashboard", "BackerDashboard"], - ["Discovery", "Activities", "Search", "LoginTout"] - ], - "Show the logged out tabs." - ) + AppEnvironment.logout() + self.vm.inputs.userSessionEnded() + + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"], + ["Discovery", "Activities", "Search", "Dashboard", "BackerDashboard"], + ["Discovery", "Activities", "Search", "LoginTout"] + ], + "Show the logged out tabs." + ) + } } - func testBackerDashboardFeatureFlagEnabled() { - let config = .template - |> Config.lens.features .~ ["ios_backer_dashboard": true] + func testSetViewControllers_WhenCreatorDashboardDisabled_NoCreatorDashboard() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [ + RemoteConfigFeature.creatorDashboardEnabled.rawValue: false + ] + let viewControllerNames = TestObserver<[String], Never>() + + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { + vm.outputs.setViewControllers.map(extractRootNames) + .observe(viewControllerNames.observer) + + self.vm.inputs.viewDidLoad() + + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"] + ], + "Show the logged out tabs." + ) + + AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) + self.vm.inputs.userSessionStarted() + + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"] + ], + "Show the logged in tabs." + ) - withEnvironment(config: config) { + AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) + self.vm.inputs.currentUserUpdated() + + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"], + ["Discovery", "Activities", "Search", "BackerDashboard"] + ], + "Don't show the creator dashboard tab." + ) + + AppEnvironment.logout() + self.vm.inputs.userSessionEnded() + + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"], + ["Discovery", "Activities", "Search", "BackerDashboard"], + ["Discovery", "Activities", "Search", "LoginTout"] + ], + "Show the logged out tabs." + ) + } + } + + func testBackerDashboardShownWithCreatorDashboard_WhenCreatorDashboardFlagEnabled() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [ + RemoteConfigFeature.creatorDashboardEnabled.rawValue: true + ] + + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { vm.inputs.viewDidLoad() AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) vm.inputs.userSessionStarted() @@ -442,6 +509,39 @@ final class RootViewModelTests: TestCase { } } + func testBackerDashboardShownWithCreatorDashboard_WhenCreatorDashboardFlagDisabled_NoCreatorDashboard() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [ + RemoteConfigFeature.creatorDashboardEnabled.rawValue: false + ] + + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { + vm.inputs.viewDidLoad() + AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) + vm.inputs.userSessionStarted() + + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"] + ], + "Show the BackerDashboard instead of Profile." + ) + + AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) + vm.inputs.currentUserUpdated() + + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"], + ["Discovery", "Activities", "Search", "BackerDashboard"] + ], + "Show the creator dashboard tab." + ) + } + } + func testViewControllersDontOverEmit() { let viewControllerNames = TestObserver<[String], Never>() vm.outputs.setViewControllers.map(extractRootNames) @@ -567,23 +667,54 @@ final class RootViewModelTests: TestCase { self.filterDiscovery.assertValues([params]) } - func testSwitchToDashboardParam() { - self.vm.inputs.viewDidLoad() + func testSwitchToDashboardParam_WhenCreatorDashboardEnabled() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [ + RemoteConfigFeature.creatorDashboardEnabled.rawValue: true + ] + + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { + self.vm.inputs.viewDidLoad() - let param = Param.id(1) + let param = Param.id(1) - AppEnvironment.login(AccessTokenEnvelope( - accessToken: "deadbeef", user: .template - |> \.stats.memberProjectsCount .~ 1 - )) - self.vm.inputs.userSessionStarted() + AppEnvironment.login(AccessTokenEnvelope( + accessToken: "deadbeef", user: .template + |> \.stats.memberProjectsCount .~ 1 + )) + self.vm.inputs.userSessionStarted() + + self.switchDashboardProject.assertValues([]) + self.vm.inputs.switchToDashboard(project: param) + + self.switchDashboardProject.assertValues([param]) + } + } + + func testSwitchToDashboardParam_WhenCreatorDashboardDisabled_NoCreatorDashboard() { + let mockRemoteConfigClient = MockRemoteConfigClient() + |> \.features .~ [ + RemoteConfigFeature.creatorDashboardEnabled.rawValue: false + ] + + withEnvironment(remoteConfigClient: mockRemoteConfigClient) { + self.vm.inputs.viewDidLoad() + + let param = Param.id(1) - self.switchDashboardProject.assertValues([]) - self.vm.inputs.switchToDashboard(project: param) - self.switchDashboardProject.assertValues([param]) + AppEnvironment.login(AccessTokenEnvelope( + accessToken: "deadbeef", user: .template + |> \.stats.memberProjectsCount .~ 1 + )) + self.vm.inputs.userSessionStarted() + + self.switchDashboardProject.assertValues([]) + self.vm.inputs.switchToDashboard(project: param) + self.switchDashboardProject.assertValues([]) + } } - func testTabBarItemStyles() { + func testTabBarItemStyles_WhenCreatorDashboardEnabled() { let user = User.template |> \.avatar.small .~ "http://image.com/image" let creator = User.template |> \.stats.memberProjectsCount .~ 1