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

[MBL-854] Gate Creator Dashboard Behind Feature Flag #1828

Merged
merged 14 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
100 changes: 68 additions & 32 deletions Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand All @@ -226,6 +234,10 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi
}

public func switchToProjectActivities(projectId: Param) {
guard featureCreatorDashboardEnabled() else {
return
}

self.switchToDashboard(project: nil)

guard
Expand All @@ -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
Comment on lines +263 to +266
Copy link
Contributor

@msadoon msadoon Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is called as a result of tabData(forUser and it checks if that tab is available before styling it. So if we sent "Explore, Activity, Search, Dashboard and Profile" to this function - the view controllers may only be "Explore, Activity, Search and Profile". Which means that Dashboard case updates its styling to reflect "Profile" and the Profile case doesn't update anything - the tab doesn't exist. self.tabBar.items is called under the hood for tabBarItem(atIndex: and reflects the view controllers - which are independently set by setViewControllers (whenever that happens).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only trade-off is that we lose the Profile user avatar, if they were a creator logged in with the dashboard hidden. I think its' worth it because this only affects logged in creators and not backers and we're removing creator login functionality anyway.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move all the profile downloading to a separate function.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function just checks if the DashboardTabViewController is available to display.

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? {
Expand Down
36 changes: 17 additions & 19 deletions Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
23 changes: 16 additions & 7 deletions Library/ViewModels/RootViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the idea is that the tabs are based on the user. And they don't need gating because this tab data is only used for styling - the actual setting of the tabs in the tab bar is done in setViewControllers.

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,
Expand Down
Loading