Move navigation coordination away from the SettingsScreenCoordinator and into the SettingsFlowCoordinator

This commit is contained in:
Stefan Ceriu
2024-01-05 12:54:57 +02:00
parent c40435b419
commit 8e4e9603e4
8 changed files with 209 additions and 223 deletions
@@ -454,6 +454,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
navigationSplitCoordinator: navigationSplitCoordinator,
windowManager: windowManager,
appLockService: appLockFlowCoordinator.appLockService,
bugReportService: ServiceLocator.shared.bugReportService,
roomTimelineControllerFactory: RoomTimelineControllerFactory(),
@@ -15,7 +15,7 @@
//
import Combine
import Foundation
import SwiftUI
enum SettingsFlowCoordinatorAction {
case presentedSettings
@@ -28,6 +28,7 @@ enum SettingsFlowCoordinatorAction {
struct SettingsFlowCoordinatorParameters {
let userSession: UserSessionProtocol
let windowManager: WindowManager
let appLockService: AppLockServiceProtocol
let bugReportService: BugReportServiceProtocol
let notificationSettings: NotificationSettingsProxyProtocol
@@ -44,6 +45,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
private var cancellables = Set<AnyCancellable>()
// periphery:ignore - retaining purpose
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
private let actionsSubject: PassthroughSubject<SettingsFlowCoordinatorAction, Never> = .init()
var actions: AnyPublisher<SettingsFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
@@ -79,14 +83,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
private func presentSettingsScreen(animated: Bool) {
navigationStackCoordinator = NavigationStackCoordinator()
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
userIndicatorController: parameters.userIndicatorController,
userSession: parameters.userSession,
appLockService: parameters.appLockService,
bugReportService: parameters.bugReportService,
notificationSettings: parameters.userSession.clientProxy.notificationSettings,
secureBackupController: parameters.userSession.clientProxy.secureBackupController,
let parameters = SettingsScreenCoordinatorParameters(userSession: parameters.userSession,
appSettings: parameters.appSettings)
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
settingsScreenCoordinator.actions
@@ -103,12 +102,30 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.actionsSubject.send(.runLogoutFlow)
}
case .clearCache:
actionsSubject.send(.clearCache)
case .secureBackup:
presentSecureBackupScreen(animated: true)
case .forceLogout:
actionsSubject.send(.forceLogout)
case .userDetails:
presentUserDetailsEditScreen()
case .accountProfile:
presentAccountProfileURL()
case .analytics:
presentAnalyticsScreen()
case .appLock:
presentAppLockSetupFlow()
case .bugReport:
presentBugReportScreen()
case .about:
presentLegalInformationScreen()
case .sessionVerification:
presentSessionVerificationScreen()
case .accountSessions:
presentAccountSessionsListURL()
case .notifications:
presentNotificationSettings()
case .advancedSettings:
presentAdvancedSettings()
case .developerOptions:
presentDeveloperOptions()
}
}
.store(in: &cancellables)
@@ -133,4 +150,151 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.push(coordinator, animated: animated)
}
private func presentUserDetailsEditScreen() {
let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(clientProxy: parameters.userSession.clientProxy,
mediaProvider: parameters.userSession.mediaProvider,
navigationStackCoordinator: navigationStackCoordinator,
userIndicatorController: parameters.userIndicatorController))
navigationStackCoordinator?.push(coordinator)
}
private func presentAnalyticsScreen() {
let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
analytics: ServiceLocator.shared.analytics))
navigationStackCoordinator?.push(coordinator)
}
private func presentAppLockSetupFlow() {
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .settings,
appLockService: parameters.appLockService,
navigationStackCoordinator: navigationStackCoordinator)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
// The flow coordinator tidies up the stack, no need to do anything.
appLockSetupFlowCoordinator = nil
case .forceLogout:
actionsSubject.send(.forceLogout)
}
}
.store(in: &cancellables)
appLockSetupFlowCoordinator = coordinator
coordinator.start()
}
private func presentBugReportScreen() {
let params = BugReportScreenCoordinatorParameters(bugReportService: parameters.bugReportService,
userID: parameters.userSession.userID,
deviceID: parameters.userSession.deviceID,
userIndicatorController: parameters.userIndicatorController,
screenshot: nil,
isModallyPresented: false)
let coordinator = BugReportScreenCoordinator(parameters: params)
coordinator.completion = { [weak self] result in
switch result {
case .finish:
self?.showSuccess(label: L10n.actionDone)
default:
break
}
self?.navigationStackCoordinator.pop()
}
navigationStackCoordinator.push(coordinator)
}
private func presentLegalInformationScreen() {
navigationStackCoordinator.push(LegalInformationScreenCoordinator(appSettings: parameters.appSettings))
}
private func presentSessionVerificationScreen() {
guard let sessionVerificationController = parameters.userSession.sessionVerificationController else {
fatalError("The sessionVerificationController should aways be valid at this point")
}
let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
let coordinator = SessionVerificationScreenCoordinator(parameters: verificationParameters)
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .done:
navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setSheetCoordinator(coordinator) { [weak self] in
self?.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
private func presentNotificationSettings() {
let notificationParameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
userSession: parameters.userSession,
userNotificationCenter: UNUserNotificationCenter.current(),
notificationSettings: parameters.notificationSettings,
isModallyPresented: false)
let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters)
navigationStackCoordinator.push(coordinator)
}
private func presentAdvancedSettings() {
let coordinator = AdvancedSettingsScreenCoordinator()
navigationStackCoordinator.push(coordinator)
}
private func presentDeveloperOptions() {
let coordinator = DeveloperOptionsScreenCoordinator()
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .clearCache:
actionsSubject.send(.clearCache)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func showSuccess(label: String) {
parameters.userIndicatorController.submitIndicator(UserIndicator(title: label))
}
// MARK: OIDC Account Management
private func presentAccountProfileURL() {
guard let url = parameters.userSession.clientProxy.accountURL(action: .profile) else {
MXLog.error("Account URL is missing.")
return
}
presentAccountManagementURL(url)
}
private func presentAccountSessionsListURL() {
guard let url = parameters.userSession.clientProxy.accountURL(action: .sessionsList) else {
MXLog.error("Account URL is missing.")
return
}
presentAccountManagementURL(url)
}
private var accountSettingsPresenter: OIDCAccountSettingsPresenter?
private func presentAccountManagementURL(_ url: URL) {
// Note to anyone in the future if you come back here to make this open in Safari instead of a WAS.
// As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷‍♂️
accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: parameters.windowManager.mainWindow)
accountSettingsPresenter?.start()
}
}
@@ -49,6 +49,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
init(userSession: UserSessionProtocol,
navigationSplitCoordinator: NavigationSplitCoordinator,
windowManager: WindowManager,
appLockService: AppLockServiceProtocol,
bugReportService: BugReportServiceProtocol,
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
@@ -75,6 +76,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
userIndicatorController: ServiceLocator.shared.userIndicatorController)
settingsFlowCoordinator = SettingsFlowCoordinator(parameters: .init(userSession: userSession,
windowManager: windowManager,
appLockService: appLockService,
bugReportService: bugReportService,
notificationSettings: userSession.clientProxy.notificationSettings,
@@ -18,32 +18,31 @@ import Combine
import SwiftUI
struct SettingsScreenCoordinatorParameters {
weak var navigationStackCoordinator: NavigationStackCoordinator?
let userIndicatorController: UserIndicatorControllerProtocol
let userSession: UserSessionProtocol
let appLockService: AppLockServiceProtocol
let bugReportService: BugReportServiceProtocol
let notificationSettings: NotificationSettingsProxyProtocol
let secureBackupController: SecureBackupControllerProtocol
let appSettings: AppSettings
}
enum SettingsScreenCoordinatorAction {
case dismiss
case logout
case clearCache
case secureBackup
/// Logout without a confirmation. The user forgot their PIN.
case forceLogout
case userDetails
case accountProfile
case analytics
case appLock
case bugReport
case about
case sessionVerification
case accountSessions
case notifications
case advancedSettings
case developerOptions
}
final class SettingsScreenCoordinator: CoordinatorProtocol {
private let parameters: SettingsScreenCoordinatorParameters
private var viewModel: SettingsScreenViewModelProtocol
// periphery:ignore - retaining purpose
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
private let actionsSubject: PassthroughSubject<SettingsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
@@ -67,29 +66,29 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
case .close:
actionsSubject.send(.dismiss)
case .userDetails:
presentUserDetailsEditScreen()
actionsSubject.send(.userDetails)
case .accountProfile:
presentAccountProfileURL()
actionsSubject.send(.accountProfile)
case .analytics:
presentAnalyticsScreen()
actionsSubject.send(.analytics)
case .appLock:
presentAppLockSetupFlow()
actionsSubject.send(.appLock)
case .reportBug:
presentBugReportScreen()
actionsSubject.send(.bugReport)
case .about:
presentLegalInformationScreen()
actionsSubject.send(.about)
case .sessionVerification:
presentSessionVerificationScreen()
actionsSubject.send(.sessionVerification)
case .secureBackup:
actionsSubject.send(.secureBackup)
case .accountSessionsList:
presentAccountSessionsListURL()
actionsSubject.send(.accountSessions)
case .notifications:
presentNotificationSettings()
actionsSubject.send(.notifications)
case .advancedSettings:
self.presentAdvancedSettings()
actionsSubject.send(.advancedSettings)
case .developerOptions:
presentDeveloperOptions()
actionsSubject.send(.developerOptions)
case .logout:
actionsSubject.send(.logout)
}
@@ -102,163 +101,4 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
func toPresentable() -> AnyView {
AnyView(SettingsScreen(context: viewModel.context))
}
// MARK: - OIDC Account Management
private func presentAccountProfileURL() {
guard let url = viewModel.context.viewState.accountProfileURL else {
MXLog.error("Account URL is missing.")
return
}
presentAccountManagementURL(url)
}
private func presentAccountSessionsListURL() {
guard let url = viewModel.context.viewState.accountSessionsListURL else {
MXLog.error("Account URL is missing.")
return
}
presentAccountManagementURL(url)
}
private var accountSettingsPresenter: OIDCAccountSettingsPresenter?
private func presentAccountManagementURL(_ url: URL) {
guard let window = viewModel.context.viewState.window else {
MXLog.error("The window is missing.")
return
}
// Note to anyone in the future if you come back here to make this open in Safari instead of a WAS.
// As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷‍♂️
accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: window)
accountSettingsPresenter?.start()
}
// MARK: - Private
private func presentUserDetailsEditScreen() {
let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(clientProxy: parameters.userSession.clientProxy,
mediaProvider: parameters.userSession.mediaProvider,
navigationStackCoordinator: parameters.navigationStackCoordinator,
userIndicatorController: parameters.userIndicatorController))
parameters.navigationStackCoordinator?.push(coordinator)
}
private func presentAnalyticsScreen() {
let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings,
analytics: ServiceLocator.shared.analytics))
parameters.navigationStackCoordinator?.push(coordinator)
}
private func presentAppLockSetupFlow() {
guard let navigationStackCoordinator = parameters.navigationStackCoordinator else {
MXLog.error("The navigation stack has gone! 🌫️")
return
}
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .settings,
appLockService: parameters.appLockService,
navigationStackCoordinator: navigationStackCoordinator)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
// The flow coordinator tidies up the stack, no need to do anything.
appLockSetupFlowCoordinator = nil
case .forceLogout:
actionsSubject.send(.forceLogout)
}
}
.store(in: &cancellables)
appLockSetupFlowCoordinator = coordinator
coordinator.start()
}
private func presentBugReportScreen() {
let params = BugReportScreenCoordinatorParameters(bugReportService: parameters.bugReportService,
userID: parameters.userSession.userID,
deviceID: parameters.userSession.deviceID,
userIndicatorController: parameters.userIndicatorController,
screenshot: nil,
isModallyPresented: false)
let coordinator = BugReportScreenCoordinator(parameters: params)
coordinator.completion = { [weak self] result in
switch result {
case .finish:
self?.showSuccess(label: L10n.actionDone)
default:
break
}
self?.parameters.navigationStackCoordinator?.pop()
}
parameters.navigationStackCoordinator?.push(coordinator)
}
private func presentLegalInformationScreen() {
parameters.navigationStackCoordinator?.push(LegalInformationScreenCoordinator(appSettings: parameters.appSettings))
}
private func presentSessionVerificationScreen() {
guard let sessionVerificationController = parameters.userSession.sessionVerificationController else {
fatalError("The sessionVerificationController should aways be valid at this point")
}
let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
let coordinator = SessionVerificationScreenCoordinator(parameters: verificationParameters)
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .done:
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
parameters.navigationStackCoordinator?.setSheetCoordinator(coordinator) { [weak self] in
self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
}
}
private func presentNotificationSettings() {
let notificationParameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator,
userSession: parameters.userSession,
userNotificationCenter: UNUserNotificationCenter.current(),
notificationSettings: parameters.notificationSettings,
isModallyPresented: false)
let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters)
parameters.navigationStackCoordinator?.push(coordinator)
}
private func presentAdvancedSettings() {
let coordinator = AdvancedSettingsScreenCoordinator()
parameters.navigationStackCoordinator?.push(coordinator)
}
private func presentDeveloperOptions() {
let coordinator = DeveloperOptionsScreenCoordinator()
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .clearCache:
actionsSubject.send(.clearCache)
}
}
.store(in: &cancellables)
parameters.navigationStackCoordinator?.push(coordinator)
}
private func showSuccess(label: String) {
parameters.userIndicatorController.submitIndicator(UserIndicator(title: label))
}
}
@@ -44,9 +44,6 @@ struct SettingsScreenViewState: BindableState {
var isSessionVerified: Bool?
var showSecureBackupBadge = false
var showDeveloperOptions: Bool
/// The presentation anchor used to display the OIDC account URL.
var window: UIWindow?
}
enum SettingsScreenViewAction {
@@ -64,7 +61,4 @@ enum SettingsScreenViewAction {
case developerOptions
case advancedSettings
case logout
/// Updates the window used for the OIDC account URL anchor.
case updateWindow(UIWindow)
}
@@ -94,12 +94,6 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
actionsSubject.send(.advancedSettings)
case .developerOptions:
actionsSubject.send(.developerOptions)
case .updateWindow(let window):
Task {
guard state.window != window else { return }
state.window = window
}
}
}
}
@@ -41,9 +41,6 @@ struct SettingsScreen: View {
.navigationTitle(L10n.commonSettings)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.introspect(.window, on: .supportedVersions) { window in
context.send(viewAction: .updateWindow(window))
}
}
private var userSection: some View {
@@ -52,7 +52,7 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate {
func start() {
guard let screenID = ProcessInfo.testScreenID else { fatalError("Unable to launch with unknown screen.") }
let mockScreen = MockScreen(id: screenID)
let mockScreen = MockScreen(id: screenID, windowManager: windowManager)
navigationRootCoordinator.setRootCoordinator(mockScreen.coordinator)
self.mockScreen = mockScreen
}
@@ -79,12 +79,12 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate {
@MainActor
class MockScreen: Identifiable {
let id: UITestsScreenIdentifier
let windowManager: WindowManager?
let windowManager: WindowManager
private var retainedState = [Any]()
private var cancellables = Set<AnyCancellable>()
init(id: UITestsScreenIdentifier, windowManager: WindowManager? = nil) {
init(id: UITestsScreenIdentifier, windowManager: WindowManager) {
self.id = id
self.windowManager = windowManager
}
@@ -209,10 +209,10 @@ class MockScreen: Identifiable {
navigationCoordinator: navigationCoordinator,
notificationCenter: notificationCenter)
guard let windowManager else { fatalError("The window manager must be supplied.") }
coordinator.actions
.sink { action in
.sink { [weak self] action in
guard let self else { return }
switch action {
case .lockApp:
windowManager.switchToAlternate()
@@ -268,16 +268,9 @@ class MockScreen: Identifiable {
case .settings:
let navigationStackCoordinator = NavigationStackCoordinator()
let clientProxy = MockClientProxy(userID: "@mock:client.com")
let coordinator = SettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
userIndicatorController: UserIndicatorControllerMock(),
userSession: MockUserSession(clientProxy: clientProxy,
let coordinator = SettingsScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy,
mediaProvider: MockMediaProvider(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock()),
appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(),
notificationSettings: NotificationSettingsProxyMock(with: .init()),
secureBackupController: SecureBackupControllerMock(),
appSettings: ServiceLocator.shared.settings))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
@@ -566,6 +559,7 @@ class MockScreen: Identifiable {
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()),
navigationSplitCoordinator: navigationSplitCoordinator,
windowManager: windowManager,
appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(),