diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 45f3c8227..32d41656e 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -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(), diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 6fc665007..1cd7673c5 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -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() + // periphery:ignore - retaining purpose + private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator? + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { 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() + } } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index c92be52f9..65b819f6b 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -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, diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index b3b32d408..d70f98ded 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -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 = .init() private var cancellables = Set() @@ -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)) - } } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index 91bc9f361..8fb6288cc 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -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) } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 9a091d380..66253331d 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -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 - } } } } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index bda6e7c79..c70f0a154 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -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 { diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index f72c6d116..43091577d 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -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() - 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(),