element-ios/Riot/Modules/Onboarding/OnboardingCoordinator.swift

252 lines
10 KiB
Swift
Raw Normal View History

// File created from FlowTemplate
// $ createRootCoordinator.sh Onboarding/SplashScreen Onboarding OnboardingSplashScreen
/*
Copyright 2021 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 UIKit
/// OnboardingCoordinator input parameters
struct OnboardingCoordinatorParameters {
/// The navigation router that manage physical navigation
let router: NavigationRouterType
2022-01-12 17:11:29 +00:00
/// The credentials to use if a soft logout has taken place.
let softLogoutCredentials: MXCredentials?
2022-01-12 17:11:29 +00:00
init(router: NavigationRouterType? = nil,
softLogoutCredentials: MXCredentials? = nil) {
self.router = router ?? NavigationRouter(navigationController: RiotNavigationController(isLockedToPortraitOnPhone: true))
2022-01-12 17:11:29 +00:00
self.softLogoutCredentials = softLogoutCredentials
}
}
@objcMembers
/// A coordinator to manage the full onboarding flow with pre-auth screens, authentication and setup screens once signed in.
final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
// MARK: - Properties
// MARK: Private
private let parameters: OnboardingCoordinatorParameters
// TODO: these can likely be consolidated using an additional authType.
/// The any registration parameters for AuthenticationViewController from a server provisioning link.
2022-01-12 17:11:29 +00:00
private var externalRegistrationParameters: [AnyHashable: Any]?
/// A custom homeserver to be shown when logging in.
2022-01-12 17:11:29 +00:00
private var customHomeserver: String?
/// A custom identity server to be used once logged in.
2022-01-12 17:11:29 +00:00
private var customIdentityServer: String?
// MARK: Navigation State
private var navigationRouter: NavigationRouterType {
parameters.router
}
// Keep a strong ref as we need to init authVC early to preload its view (it is *really* slow to do in realtime)
private var authenticationCoordinator: AuthenticationCoordinatorProtocol = AuthenticationCoordinator()
private var isShowingAuthentication = false
// MARK: Screen results
2022-01-12 17:11:29 +00:00
private var splashScreenResult: OnboardingSplashScreenViewModelResult?
private var useCaseResult: OnboardingUseCaseViewModelResult?
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
2022-01-12 17:11:29 +00:00
var completion: (() -> Void)?
// MARK: - Setup
init(parameters: OnboardingCoordinatorParameters) {
self.parameters = parameters
2022-01-12 17:11:29 +00:00
super.init()
}
// MARK: - Public
func start() {
// TODO: Manage a separate flow for soft logout that just uses AuthenticationCoordinator
if #available(iOS 14.0, *), parameters.softLogoutCredentials == nil, BuildSettings.authScreenShowRegister {
2022-01-12 17:11:29 +00:00
showSplashScreen()
} else {
showAuthenticationScreen()
}
2022-01-12 17:11:29 +00:00
}
func toPresentable() -> UIViewController {
navigationRouter.toPresentable()
}
/// Force a registration process based on a predefined set of parameters from a server provisioning link.
/// For more information see `AuthenticationViewController.externalRegistrationParameters`.
2022-01-12 17:11:29 +00:00
func update(externalRegistrationParameters: [AnyHashable: Any]) {
self.externalRegistrationParameters = externalRegistrationParameters
authenticationCoordinator.update(externalRegistrationParameters: externalRegistrationParameters)
2022-01-12 17:11:29 +00:00
}
/// Set up the authentication screen with the specified homeserver and/or identity server.
func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) {
2022-01-12 17:11:29 +00:00
self.customHomeserver = homeserver
self.customIdentityServer = identityServer
authenticationCoordinator.updateHomeserver(homeserver, andIdentityServer: identityServer)
2022-01-12 17:11:29 +00:00
}
/// When SSO login succeeded, when SFSafariViewController is used, continue login with success parameters.
2022-01-12 17:11:29 +00:00
func continueSSOLogin(withToken loginToken: String, transactionID: String) -> Bool {
guard isShowingAuthentication else { return false }
2022-01-12 17:11:29 +00:00
return authenticationCoordinator.continueSSOLogin(withToken: loginToken, transactionID: transactionID)
}
// MARK: - Private
2022-01-12 17:11:29 +00:00
@available(iOS 14.0, *)
/// Show the onboarding splash screen as the root module in the flow.
2022-01-12 17:11:29 +00:00
private func showSplashScreen() {
let coordinator = OnboardingSplashScreenCoordinator()
2022-01-12 17:11:29 +00:00
coordinator.completion = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
self.splashScreenCoordinator(coordinator, didCompleteWith: result)
}
2022-01-12 17:11:29 +00:00
coordinator.start()
add(childCoordinator: coordinator)
self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
}
@available(iOS 14.0, *)
/// Displays the next view in the flow after the splash screen.
2022-01-12 17:11:29 +00:00
private func splashScreenCoordinator(_ coordinator: OnboardingSplashScreenCoordinator, didCompleteWith result: OnboardingSplashScreenViewModelResult) {
splashScreenResult = result
// Set the auth type early to allow network requests to finish during display of the use case screen.
let mxkAuthenticationType = splashScreenResult == .register ? MXKAuthenticationTypeRegister : MXKAuthenticationTypeLogin
authenticationCoordinator.update(authenticationType: mxkAuthenticationType)
switch result {
case .register:
showUseCase()
case .login:
showAuthenticationScreen()
}
}
@available(iOS 14.0, *)
/// Show the use case screen for new users.
private func showUseCase() {
let coordinator = OnboardingUseCaseCoordinator()
coordinator.completion = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
self.useCaseCoordinator(coordinator, didCompleteWith: result)
}
coordinator.start()
add(childCoordinator: coordinator)
if self.navigationRouter.modules.isEmpty {
self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
} else {
self.navigationRouter.push(coordinator, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
}
/// Displays the next view in the flow after the use case screen.
private func useCaseCoordinator(_ coordinator: OnboardingUseCaseCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) {
useCaseResult = result
showAuthenticationScreen()
2022-01-12 17:11:29 +00:00
}
/// Show the authentication screen. Any parameters that have been set in previous screens are be applied.
private func showAuthenticationScreen() {
guard !isShowingAuthentication else { return }
2022-01-12 17:11:29 +00:00
MXLog.debug("[OnboardingCoordinator] showAuthenticationScreen")
let coordinator = authenticationCoordinator
coordinator.completion = { [weak self, weak coordinator] authenticationType in
2022-01-12 17:11:29 +00:00
guard let self = self, let coordinator = coordinator else { return }
self.authenticationCoordinator(coordinator, didCompleteWith: authenticationType)
2022-01-12 17:11:29 +00:00
}
// Due to needing to preload the authVC, this breaks the Coordinator init/start pattern.
// This can be re-assessed once we re-write a native flow for authentication.
if let externalRegistrationParameters = externalRegistrationParameters {
coordinator.update(externalRegistrationParameters: externalRegistrationParameters)
}
if useCaseResult == .customServer {
coordinator.showCustomServer()
}
if let softLogoutCredentials = parameters.softLogoutCredentials {
coordinator.update(softLogoutCredentials: softLogoutCredentials)
}
2022-01-12 17:11:29 +00:00
coordinator.start()
add(childCoordinator: coordinator)
authenticationCoordinator = coordinator
if customHomeserver != nil || customIdentityServer != nil {
coordinator.updateHomeserver(customHomeserver, andIdentityServer: customIdentityServer)
2022-01-12 17:11:29 +00:00
}
if self.navigationRouter.modules.isEmpty {
self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
} else {
self.navigationRouter.push(coordinator, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
self?.isShowingAuthentication = false
}
2022-01-12 17:11:29 +00:00
}
isShowingAuthentication = true
2022-01-12 17:11:29 +00:00
}
/// Displays the next view in the flow after the authentication screen.
private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol, didCompleteWith authenticationType: MXKAuthenticationType) {
2022-01-12 17:11:29 +00:00
completion?()
isShowingAuthentication = false
// Handle the chosen use case if appropriate
if authenticationType == MXKAuthenticationTypeRegister,
let useCaseResult = useCaseResult,
let userSession = UserSessionsService.shared.mainUserSession {
// Store the value in the user's session
userSession.properties.useCase = useCaseResult.userSessionPropertyValue
}
}
}
extension OnboardingUseCaseViewModelResult {
/// The result converted into the type stored in the user session.
var userSessionPropertyValue: UserSessionProperties.UseCase? {
switch self {
case .personalMessaging:
return .personalMessaging
case .workMessaging:
return .workMessaging
case .communityMessaging:
return .communityMessaging
case .skipped:
return .skipped
case .customServer:
return nil
}
}
}