diff --git a/Core/Core/Common/CommonModels/AppEnvironment/ExperimentalFeature.swift b/Core/Core/Common/CommonModels/AppEnvironment/ExperimentalFeature.swift index 81e1d4c07e..487fe53ded 100644 --- a/Core/Core/Common/CommonModels/AppEnvironment/ExperimentalFeature.swift +++ b/Core/Core/Common/CommonModels/AppEnvironment/ExperimentalFeature.swift @@ -30,7 +30,7 @@ public enum ExperimentalFeature: String, CaseIterable, Codable { case whatIfScore = "what_if_score" case rebuiltCalendar = "rebuilt_calendar" case revertToOldStudentToDo = "revert_to_old_student_todo" - case studentLearnerDashboard = "student_learner_dashboard" + case revertToOldStudentDashboard = "revert_to_old_student_dashboard" private static var sharedUserDefaults: UserDefaults { UserDefaults(suiteName: Bundle.main.appGroupID()) ?? .standard diff --git a/Core/Core/Common/CommonModels/AppEnvironment/FeatureFlags/GetEnvironmentFeatureFlags.swift b/Core/Core/Common/CommonModels/AppEnvironment/FeatureFlags/GetEnvironmentFeatureFlags.swift index 667d50e234..0bba56de4b 100644 --- a/Core/Core/Common/CommonModels/AppEnvironment/FeatureFlags/GetEnvironmentFeatureFlags.swift +++ b/Core/Core/Common/CommonModels/AppEnvironment/FeatureFlags/GetEnvironmentFeatureFlags.swift @@ -24,6 +24,7 @@ public enum EnvironmentFeatureFlags: String { case mobile_offline_mode case account_survey_notifications case restrict_student_access + case widget_dashboard } public class GetEnvironmentFeatureFlags: CollectionUseCase { diff --git a/Core/Core/Common/CommonModels/AppEnvironment/SessionDefaults.swift b/Core/Core/Common/CommonModels/AppEnvironment/SessionDefaults.swift index 160940f56e..a2ed887905 100644 --- a/Core/Core/Common/CommonModels/AppEnvironment/SessionDefaults.swift +++ b/Core/Core/Common/CommonModels/AppEnvironment/SessionDefaults.swift @@ -319,6 +319,8 @@ public struct SessionDefaults: Equatable { set { self["isSpeedGraderAnnotationToolbarVisible"] = newValue } } + // MARK: - Learner Dashboard + public var preferNewLearnerDashboard: Bool { get { (self["preferNewLearnerDashboard"] as? Bool) ?? true diff --git a/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift b/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift index 3f8d341c55..819e72cb97 100644 --- a/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift +++ b/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift @@ -49,17 +49,28 @@ public struct DashboardContainerView: View, ScreenViewTrackable { private let shouldShowGroupList: Bool private let verticalSpacing: CGFloat = 16 - public init(shouldShowGroupList: Bool, - showOnlyTeacherEnrollment: Bool, - offlineViewModel: OfflineModeViewModel = OfflineModeViewModel(interactor: OfflineModeAssembly.make())) { + public init( + shouldShowGroupList: Bool, + showOnlyTeacherEnrollment: Bool, + isLearnerDashboardEnabledOnInstance: Bool, + offlineViewModel: OfflineModeViewModel = OfflineModeViewModel(interactor: OfflineModeAssembly.make()) + ) { courseCardListViewModel = DashboardCourseCardListAssembly.makeDashboardCourseCardListViewModel(showOnlyTeacherEnrollment: showOnlyTeacherEnrollment) self.shouldShowGroupList = shouldShowGroupList let env = AppEnvironment.shared - layoutViewModel = DashboardLayoutViewModel(interactor: DashboardSettingsInteractorLive(environment: env, defaults: env.userDefaults)) + layoutViewModel = DashboardLayoutViewModel(interactor: DashboardSettingsInteractorLive( + environment: env, + defaults: env.userDefaults, + isLearnerDashboardEnabledOnInstance: isLearnerDashboardEnabledOnInstance + )) colors = env.subscribe(GetCustomColors()) notifications = env.subscribe(GetAccountNotifications()) settings = env.subscribe(GetUserSettings(userID: "self")) - _viewModel = StateObject(wrappedValue: DashboardContainerViewModel(environment: env, defaults: env.userDefaults ?? .fallback)) + _viewModel = StateObject(wrappedValue: DashboardContainerViewModel( + environment: env, + defaults: env.userDefaults ?? .fallback, + isLearnerDashboardEnabledOnInstance: isLearnerDashboardEnabledOnInstance + )) self.offlineModeViewModel = offlineViewModel } diff --git a/Core/Core/Features/Dashboard/Container/ViewModel/DashboardContainerViewModel.swift b/Core/Core/Features/Dashboard/Container/ViewModel/DashboardContainerViewModel.swift index 7968248c9d..82885717af 100644 --- a/Core/Core/Features/Dashboard/Container/ViewModel/DashboardContainerViewModel.swift +++ b/Core/Core/Features/Dashboard/Container/ViewModel/DashboardContainerViewModel.swift @@ -39,13 +39,18 @@ public class DashboardContainerViewModel: ObservableObject { public init( environment: AppEnvironment, defaults: SessionDefaults, + isLearnerDashboardEnabledOnInstance: Bool = false, courseSyncInteractor: CourseSyncInteractor = CourseSyncDownloaderAssembly.makeInteractor() ) { self.defaults = defaults self.environment = environment settingsButtonTapped .map { - let interactor = DashboardSettingsInteractorLive(environment: environment, defaults: environment.userDefaults) + let interactor = DashboardSettingsInteractorLive( + environment: environment, + defaults: environment.userDefaults, + isLearnerDashboardEnabledOnInstance: isLearnerDashboardEnabledOnInstance + ) let viewModel = DashboardSettingsViewModel(interactor: interactor) let dashboard = CoreHostingController(DashboardSettingsView(viewModel: viewModel)) dashboard.addDoneButton(side: .right) diff --git a/Core/Core/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLive.swift b/Core/Core/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLive.swift index 5c4fde91d9..a576aa0cf8 100644 --- a/Core/Core/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLive.swift +++ b/Core/Core/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLive.swift @@ -37,7 +37,7 @@ public class DashboardSettingsInteractorLive: DashboardSettingsInteractor { private var subscriptions = Set() private var userSettings: Store! - public init(environment: AppEnvironment, defaults: SessionDefaults?) { + public init(environment: AppEnvironment, defaults: SessionDefaults?, isLearnerDashboardEnabledOnInstance: Bool = false) { let defaults = defaults ?? .fallback let storedLayout: DashboardLayout = defaults.isDashboardLayoutGrid ? .grid : .list self.defaults = defaults @@ -47,7 +47,7 @@ public class DashboardSettingsInteractorLive: DashboardSettingsInteractor { self.useNewDashboard = CurrentValueSubject(defaults.preferNewLearnerDashboard) self.isGradesSwitchVisible = (environment.app == .student) self.isColorOverlaySwitchVisible = (environment.app == .student || environment.app == .teacher) - self.isNewDashboardSwitchVisible = ExperimentalFeature.studentLearnerDashboard.isEnabled + self.isNewDashboardSwitchVisible = !ExperimentalFeature.revertToOldStudentDashboard.isEnabled && isLearnerDashboardEnabledOnInstance self.userSettings = environment.subscribe(GetUserSettings(userID: "self")) { [weak self] in self?.updateColorOverlay() } diff --git a/Core/CoreTests/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLiveTests.swift b/Core/CoreTests/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLiveTests.swift index 848a0b924d..2ff8c0a1a7 100644 --- a/Core/CoreTests/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLiveTests.swift +++ b/Core/CoreTests/Features/Dashboard/Settings/Model/DashboardSettingsInteractorLiveTests.swift @@ -66,6 +66,18 @@ class DashboardSettingsInteractorLiveTests: CoreTestCase { XCTAssertFalse(testee.colorOverlay.value) } + func testNewDashboardSwitchNotVisible() { + defaults.showGradesOnDashboard = false + let testee = DashboardSettingsInteractorLive(environment: environment, defaults: defaults) + XCTAssertFalse(testee.isNewDashboardSwitchVisible) + } + + func testNewDashboardSwitchVisible() { + defaults.showGradesOnDashboard = false + let testee = DashboardSettingsInteractorLive(environment: environment, defaults: defaults, isLearnerDashboardEnabledOnInstance: true) + XCTAssertTrue(testee.isNewDashboardSwitchVisible) + } + func testTeacherSwitchVisibility() { environment.app = .teacher let testee = DashboardSettingsInteractorLive(environment: environment, defaults: defaults) diff --git a/Student/Student/StudentAppDelegate.swift b/Student/Student/StudentAppDelegate.swift index 11c7630a2f..002f5c2b0d 100644 --- a/Student/Student/StudentAppDelegate.swift +++ b/Student/Student/StudentAppDelegate.swift @@ -46,10 +46,11 @@ class StudentAppDelegate: UIResponder, UIApplicationDelegate, AppEnvironmentDele return env }() - private var environmentFeatureFlags: Store? private var shouldSetK5StudentView = false private var backgroundFileSubmissionAssembly: FileSubmissionAssembly? + private var isLearnerDashboardEnabledOnInstance: Bool = false + private lazy var todoWidgetRouter = WidgetRouter.createTodoRouter() private lazy var gradeListWidgetRouter = WidgetRouter.createGradeListRouter() private lazy var courseGradeWidgetRouter = WidgetRouter.createCourseGradeRouter() @@ -125,7 +126,10 @@ class StudentAppDelegate: UIResponder, UIApplicationDelegate, AppEnvironmentDele let userProfile = list.first return unownedSelf.setupUserEnvironment() .flatMap { _ in unownedSelf.getFeatureFlags() } - .map { unownedSelf.initializeTracking(environmentFeatureFlags: $0) } + .map { featureFlags in + unownedSelf.isLearnerDashboardEnabledOnInstance = featureFlags.isFeatureEnabled(.widget_dashboard) + unownedSelf.initializeTracking(environmentFeatureFlags: featureFlags) + } .map { _ in unownedSelf.requestNotificationAuthorizationForUITests() } .map { _ in unownedSelf.setK5StudentViewIfNeeded(userProfile: userProfile) } .flatMap { _ in unownedSelf.showLanguageAlertIfNeeded(locale: userProfile?.locale ?? session.locale) } @@ -412,7 +416,11 @@ extension StudentAppDelegate { AppEnvironment.shared.experience .dropFirst() .sink { [weak self] in - self?.setTabBarControllerFor(experience: $0, isStartup: false, session: nil) + self?.setTabBarControllerFor( + experience: $0, + isStartup: false, + session: nil + ) } .store(in: &subscriptions) } @@ -430,7 +438,7 @@ extension StudentAppDelegate { appearance.tintColor = nil appearance.titleTextAttributes = nil - let controller = StudentTabBarController() + let controller = StudentTabBarController(isLearnerDashboardEnabledOnInstance: isLearnerDashboardEnabledOnInstance) controller.view.layoutIfNeeded() UIView.transition(with: window, duration: 0.5, options: .transitionFlipFromRight, animations: { window.rootViewController = controller diff --git a/Student/Student/StudentTabBarController.swift b/Student/Student/StudentTabBarController.swift index 27e24c5e2a..68c58ebdbc 100644 --- a/Student/Student/StudentTabBarController.swift +++ b/Student/Student/StudentTabBarController.swift @@ -22,6 +22,16 @@ import Core class StudentTabBarController: UITabBarController, SnackBarProvider { let snackBarViewModel = SnackBarViewModel() private var previousSelectedIndex = 0 + private var isLearnerDashboardEnabledOnInstance: Bool + + init(isLearnerDashboardEnabledOnInstance: Bool) { + self.isLearnerDashboardEnabledOnInstance = isLearnerDashboardEnabledOnInstance + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func viewDidLoad() { super.viewDidLoad() @@ -97,15 +107,22 @@ class StudentTabBarController: UITabBarController, SnackBarProvider { tabBarImageSelected = .homeroomTabActive } else { let defaults = AppEnvironment.shared.userDefaults ?? .fallback + + let isNewDashboardDisabledByRemoteConfig = !ExperimentalFeature.revertToOldStudentDashboard.isEnabled let preferNewDashboard = defaults.preferNewLearnerDashboard - let shouldShowNewDashboard = ExperimentalFeature.studentLearnerDashboard.isEnabled && preferNewDashboard + + let shouldShowNewDashboard = isNewDashboardDisabledByRemoteConfig && isLearnerDashboardEnabledOnInstance && preferNewDashboard if shouldShowNewDashboard { let dashboard = CoreHostingController(LearnerDashboardAssembly.makeScreen()) result = DashboardContainerViewController(rootViewController: dashboard) { CoreSplitViewController() } } else { let dashboard = CoreHostingController( - DashboardContainerView(shouldShowGroupList: true, showOnlyTeacherEnrollment: false) + DashboardContainerView( + shouldShowGroupList: true, + showOnlyTeacherEnrollment: false, + isLearnerDashboardEnabledOnInstance: isLearnerDashboardEnabledOnInstance + ) ) result = DashboardContainerViewController(rootViewController: dashboard) { if #available(iOS 26, *) { diff --git a/Teacher/Teacher/TeacherTabBarController.swift b/Teacher/Teacher/TeacherTabBarController.swift index e45fff2dd1..deecbe2b06 100644 --- a/Teacher/Teacher/TeacherTabBarController.swift +++ b/Teacher/Teacher/TeacherTabBarController.swift @@ -69,8 +69,13 @@ class TeacherTabBarController: UITabBarController, SnackBarProvider { } func coursesTab() -> UIViewController { - let cardView = CoreHostingController(DashboardContainerView(shouldShowGroupList: false, - showOnlyTeacherEnrollment: true)) + let cardView = CoreHostingController( + DashboardContainerView( + shouldShowGroupList: false, + showOnlyTeacherEnrollment: true, + isLearnerDashboardEnabledOnInstance: false + ) + ) let dashboard = DashboardContainerViewController(rootViewController: cardView) { if #available(iOS 26, *) { return CoreSplitViewController(style: .doubleColumn)