element-ios/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift

399 lines
18 KiB
Swift
Raw Normal View History

2022-09-27 07:17:22 +00:00
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CommonKit
2022-10-05 11:35:32 +00:00
import Foundation
struct UserSessionsFlowCoordinatorParameters {
let session: MXSession
let router: NavigationRouterType
}
final class UserSessionsFlowCoordinator: Coordinator, Presentable {
private let parameters: UserSessionsFlowCoordinatorParameters
private let allSessionsService: UserSessionsOverviewService
2022-10-05 11:35:32 +00:00
private let navigationRouter: NavigationRouterType
2022-10-05 11:35:32 +00:00
private var reauthenticationPresenter: ReauthenticationCoordinatorBridgePresenter?
private var signOutFlowPresenter: SignOutFlowPresenter?
2022-10-05 11:35:32 +00:00
private var errorPresenter: MXKErrorPresentation
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
/// The root coordinator for user session management.
private weak var sessionsOverviewCoordinator: UserSessionsOverviewCoordinator?
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
init(parameters: UserSessionsFlowCoordinatorParameters) {
self.parameters = parameters
2022-10-05 11:35:32 +00:00
let dataProvider = UserSessionsDataProvider(session: parameters.session)
allSessionsService = UserSessionsOverviewService(dataProvider: dataProvider)
navigationRouter = parameters.router
2022-10-05 11:35:32 +00:00
errorPresenter = MXKErrorAlertPresentation()
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.router.toPresentable())
}
2022-09-22 08:52:42 +00:00
// MARK: - Private
private func pushScreen(with coordinator: Coordinator & Presentable) {
add(childCoordinator: coordinator)
navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in
2022-09-22 08:52:42 +00:00
self?.remove(childCoordinator: coordinator)
})
coordinator.start()
}
private func createUserSessionsOverviewCoordinator() -> UserSessionsOverviewCoordinator {
let parameters = UserSessionsOverviewCoordinatorParameters(session: parameters.session,
service: allSessionsService)
2022-09-22 08:52:42 +00:00
let coordinator = UserSessionsOverviewCoordinator(parameters: parameters)
coordinator.completion = { [weak self] result in
guard let self = self else { return }
switch result {
case .verifyCurrentSession:
self.showCompleteSecurity()
2022-10-05 11:35:32 +00:00
case let .renameSession(sessionInfo):
self.showRenameSessionScreen(for: sessionInfo)
2022-10-05 11:35:32 +00:00
case let .logoutOfSession(sessionInfo):
self.showLogoutConfirmation(for: sessionInfo)
2022-10-03 11:42:30 +00:00
case let .openSessionOverview(sessionInfo: sessionInfo):
2022-10-04 12:34:31 +00:00
self.openSessionOverview(sessionInfo: sessionInfo)
2022-10-05 13:59:15 +00:00
case let .openOtherSessions(sessionInfos: sessionInfos, filter: filter):
self.openOtherSessions(sessionInfos: sessionInfos, filterBy: filter)
case .linkDevice:
self.openQRLoginScreen()
2022-09-22 08:52:42 +00:00
}
}
return coordinator
}
private func openSessionDetails(sessionInfo: UserSessionInfo) {
let coordinator = createUserSessionDetailsCoordinator(sessionInfo: sessionInfo)
2022-09-22 08:52:42 +00:00
pushScreen(with: coordinator)
}
private func createUserSessionDetailsCoordinator(sessionInfo: UserSessionInfo) -> UserSessionDetailsCoordinator {
let parameters = UserSessionDetailsCoordinatorParameters(sessionInfo: sessionInfo)
2022-09-22 08:52:42 +00:00
return UserSessionDetailsCoordinator(parameters: parameters)
}
2022-10-04 12:34:31 +00:00
private func openSessionOverview(sessionInfo: UserSessionInfo) {
let coordinator = createUserSessionOverviewCoordinator(sessionInfo: sessionInfo)
2022-09-23 14:16:18 +00:00
coordinator.completion = { [weak self] result in
guard let self = self else { return }
switch result {
2022-10-04 12:34:31 +00:00
case let .openSessionDetails(sessionInfo: sessionInfo):
self.openSessionDetails(sessionInfo: sessionInfo)
case let .verifySession(sessionInfo):
if sessionInfo.isCurrent {
self.showCompleteSecurity()
} else {
self.showVerification(for: sessionInfo)
}
2022-10-05 11:35:32 +00:00
case let .renameSession(sessionInfo):
self.showRenameSessionScreen(for: sessionInfo)
2022-10-05 11:35:32 +00:00
case let .logoutOfSession(sessionInfo):
self.showLogoutConfirmation(for: sessionInfo)
2022-09-23 14:16:18 +00:00
}
}
pushScreen(with: coordinator)
}
/// Shows the QR login screen.
private func openQRLoginScreen() {
let service = QRLoginService(client: parameters.session.matrixRestClient,
mode: .authenticated)
let parameters = AuthenticationQRLoginStartCoordinatorParameters(navigationRouter: navigationRouter,
qrLoginService: service)
let coordinator = AuthenticationQRLoginStartCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] _ in
guard let self = self, let coordinator = coordinator else { return }
self.remove(childCoordinator: coordinator)
}
pushScreen(with: coordinator)
}
2022-09-23 14:16:18 +00:00
2022-10-04 12:34:31 +00:00
private func createUserSessionOverviewCoordinator(sessionInfo: UserSessionInfo) -> UserSessionOverviewCoordinator {
let parameters = UserSessionOverviewCoordinatorParameters(session: parameters.session,
sessionInfo: sessionInfo,
sessionsOverviewDataPublisher: allSessionsService.overviewDataPublisher)
2022-09-23 14:16:18 +00:00
return UserSessionOverviewCoordinator(parameters: parameters)
}
private func openOtherSessions(sessionInfos: [UserSessionInfo], filterBy filter: UserOtherSessionsFilter) {
2022-10-07 14:00:15 +00:00
let title = filter == .all ? VectorL10n.userSessionsOverviewOtherSessionsSectionTitle : VectorL10n.userOtherSessionSecurityRecommendationTitle
2022-10-05 13:59:15 +00:00
let coordinator = createOtherSessionsCoordinator(sessionInfos: sessionInfos,
2022-09-30 13:34:41 +00:00
filterBy: filter,
title: title)
2022-10-04 06:38:53 +00:00
coordinator.completion = { [weak self] result in
guard let self = self else { return }
switch result {
case let .openSessionDetails(sessionInfo: session):
self.openSessionDetails(sessionInfo: session)
2022-10-04 06:38:53 +00:00
}
2022-09-30 13:34:41 +00:00
}
pushScreen(with: coordinator)
}
2022-10-05 13:59:15 +00:00
private func createOtherSessionsCoordinator(sessionInfos: [UserSessionInfo],
filterBy filter: UserOtherSessionsFilter,
2022-10-04 12:44:09 +00:00
title: String) -> UserOtherSessionsCoordinator {
2022-10-05 13:59:15 +00:00
let parameters = UserOtherSessionsCoordinatorParameters(sessionInfos: sessionInfos,
2022-09-30 13:34:41 +00:00
filter: filter,
title: title)
return UserOtherSessionsCoordinator(parameters: parameters)
}
2022-10-05 11:35:32 +00:00
/// Shows a confirmation dialog to the user to sign out of a session.
private func showLogoutConfirmation(for sessionInfo: UserSessionInfo) {
guard !sessionInfo.isCurrent else {
showLogoutConfirmationForCurrentSession()
return
}
2022-10-05 11:35:32 +00:00
// Use a UIAlertController as we don't have confirmationDialog in SwiftUI on iOS 14.
let alert = UIAlertController(title: VectorL10n.signOutConfirmationMessage, message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: VectorL10n.signOut, style: .destructive) { [weak self] _ in
self?.showLogoutAuthentication(for: sessionInfo)
})
alert.addAction(UIAlertAction(title: VectorL10n.cancel, style: .cancel))
alert.popoverPresentationController?.sourceView = toPresentable().view
2022-10-05 11:35:32 +00:00
navigationRouter.present(alert, animated: true)
}
private func showLogoutConfirmationForCurrentSession() {
let flowPresenter = SignOutFlowPresenter(session: parameters.session, presentingViewController: toPresentable())
flowPresenter.delegate = self
flowPresenter.start()
signOutFlowPresenter = flowPresenter
}
2022-10-05 11:35:32 +00:00
/// Prompts the user to authenticate (if necessary) in order to log out of a specific session.
private func showLogoutAuthentication(for sessionInfo: UserSessionInfo) {
startLoading()
let deleteDeviceRequest = AuthenticatedEndpointRequest.deleteDevice(sessionInfo.id)
2022-10-05 11:35:32 +00:00
let coordinatorParameters = ReauthenticationCoordinatorParameters(session: parameters.session,
presenter: navigationRouter.toPresentable(),
title: VectorL10n.deviceDetailsDeletePromptTitle,
message: VectorL10n.deviceDetailsDeletePromptMessage,
authenticatedEndpointRequest: deleteDeviceRequest)
let presenter = ReauthenticationCoordinatorBridgePresenter()
presenter.present(with: coordinatorParameters, animated: true) { [weak self] authenticationParameters in
self?.finalizeLogout(of: sessionInfo, with: authenticationParameters)
self?.reauthenticationPresenter = nil
} cancel: { [weak self] in
self?.stopLoading()
self?.reauthenticationPresenter = nil
} failure: { [weak self] error in
guard let self = self else { return }
self.stopLoading()
self.errorPresenter.presentError(from: self.toPresentable(), forError: error, animated: true, handler: { })
self.reauthenticationPresenter = nil
}
reauthenticationPresenter = presenter
}
/// Finishes the logout process by deleting the device from the user's account.
/// - Parameters:
/// - sessionInfo: The `UserSessionInfo` for the session to be removed.
/// - authenticationParameters: The parameters from performing interactive authentication on the `devices` endpoint.
private func finalizeLogout(of sessionInfo: UserSessionInfo, with authenticationParameters: [String: Any]?) {
parameters.session.matrixRestClient.deleteDevice(sessionInfo.id,
authParameters: authenticationParameters ?? [:]) { [weak self] response in
guard let self = self else { return }
self.stopLoading()
guard response.isSuccess else {
MXLog.debug("[UserSessionsFlowCoordinator] Delete device (\(sessionInfo.id)) failed")
2022-10-05 11:35:32 +00:00
if let error = response.error {
self.errorPresenter.presentError(from: self.toPresentable(), forError: error, animated: true, handler: { })
} else {
self.errorPresenter.presentGenericError(from: self.toPresentable(), animated: true, handler: { })
}
return
}
self.popToSessionsOverview()
}
}
private func showRenameSessionScreen(for sessionInfo: UserSessionInfo) {
let parameters = UserSessionNameCoordinatorParameters(session: parameters.session, sessionInfo: sessionInfo)
let coordinator = UserSessionNameCoordinator(parameters: parameters)
coordinator.completion = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
switch result {
case .sessionNameUpdated:
self.allSessionsService.updateOverviewData { [weak self] _ in
self?.navigationRouter.dismissModule(animated: true, completion: nil)
self?.remove(childCoordinator: coordinator)
}
case .cancel:
self.navigationRouter.dismissModule(animated: true, completion: nil)
self.remove(childCoordinator: coordinator)
}
}
add(childCoordinator: coordinator)
let modalRouter = NavigationRouter(navigationController: RiotNavigationController())
modalRouter.setRootModule(coordinator)
coordinator.start()
navigationRouter.present(modalRouter, animated: true)
}
/// Shows a prompt to the user that it is not possible to verify
/// another session until the current session has been verified.
private func showCannotVerifyOtherSessionPrompt() {
let alert = UIAlertController(title: VectorL10n.securitySettingsCompleteSecurityAlertTitle,
message: VectorL10n.securitySettingsCompleteSecurityAlertMessage,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: VectorL10n.later, style: .cancel))
alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default) { [weak self] _ in
self?.showCompleteSecurity()
})
navigationRouter.present(alert, animated: true)
}
/// Shows the Complete Security modal for the user to verify their current session.
private func showCompleteSecurity() {
AppDelegate.theDelegate().presentCompleteSecurity(for: parameters.session)
}
/// Shows the verification screen for the specified session.
private func showVerification(for sessionInfo: UserSessionInfo) {
if sessionInfo.verificationState == .unknown {
showCannotVerifyOtherSessionPrompt()
return
}
let coordinator = UserVerificationCoordinator(presenter: toPresentable(),
session: parameters.session,
userId: parameters.session.myUserId,
userDisplayName: nil,
deviceId: sessionInfo.id)
coordinator.delegate = self
add(childCoordinator: coordinator)
coordinator.start()
}
2022-10-05 11:35:32 +00:00
/// Pops back to the root coordinator in the session management flow.
private func popToSessionsOverview() {
guard let sessionsOverviewCoordinator = sessionsOverviewCoordinator else { return }
navigationRouter.popToModule(sessionsOverviewCoordinator, animated: true)
}
/// Show an activity indicator whilst loading.
private func startLoading() {
loadingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: true))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}
// MARK: - Public
func start() {
MXLog.debug("[UserSessionsFlowCoordinator] did start.")
2022-09-22 08:52:42 +00:00
let rootCoordinator = createUserSessionsOverviewCoordinator()
rootCoordinator.start()
2022-09-22 08:52:42 +00:00
add(childCoordinator: rootCoordinator)
2022-09-22 08:52:42 +00:00
if navigationRouter.modules.isEmpty == false {
navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
self?.completion?()
})
} else {
navigationRouter.setRootModule(rootCoordinator) { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
self?.completion?()
}
}
2022-10-05 11:35:32 +00:00
sessionsOverviewCoordinator = rootCoordinator
}
func toPresentable() -> UIViewController {
2022-09-27 07:17:22 +00:00
navigationRouter.toPresentable()
}
}
// MARK: SignOutFlowPresenter
extension UserSessionsFlowCoordinator: SignOutFlowPresenterDelegate {
func signOutFlowPresenterDidStartLoading(_ presenter: SignOutFlowPresenter) {
startLoading()
}
func signOutFlowPresenterDidStopLoading(_ presenter: SignOutFlowPresenter) {
stopLoading()
}
func signOutFlowPresenter(_ presenter: SignOutFlowPresenter, didFailWith error: Error) {
errorPresenter.presentError(from: toPresentable(), forError: error, animated: true, handler: { })
}
}
// MARK: CrossSigningSetupCoordinatorDelegate
extension UserSessionsFlowCoordinator: CrossSigningSetupCoordinatorDelegate {
func crossSigningSetupCoordinatorDidComplete(_ coordinator: CrossSigningSetupCoordinatorType) {
// The service is listening for changes so there's nothing to do here.
remove(childCoordinator: coordinator)
}
func crossSigningSetupCoordinatorDidCancel(_ coordinator: CrossSigningSetupCoordinatorType) {
remove(childCoordinator: coordinator)
}
func crossSigningSetupCoordinator(_ coordinator: CrossSigningSetupCoordinatorType, didFailWithError error: Error) {
remove(childCoordinator: coordinator)
errorPresenter.presentError(from: toPresentable(), forError: error, animated: true, handler: { })
}
}
// MARK: UserVerificationCoordinatorDelegate
extension UserSessionsFlowCoordinator: UserVerificationCoordinatorDelegate {
func userVerificationCoordinatorDidComplete(_ coordinator: UserVerificationCoordinatorType) {
// The service is listening for changes so there's nothing to do here.
remove(childCoordinator: coordinator)
}
}