mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +00:00
In preparation of FTUE changes... (#2556)
* Remove the welcome screen * Allow the UserSessionFlowCoordinator to control the whole app hierarchy, not only its splitView * Start using the new verification state listener * Rename Onboarding to AuthenticationStart in preparation for the new OnboardingFlowCoordinator; update snapshots and tests * Make the AuthenticationCoordinator a proper FlowCoordinator * Add some padding around the authentication start screen report a problem button * Bump the RustSDK to v1.1.49 * Only add bottom padding on the authentication start screen report a problem button
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
//
|
||||
// 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
protocol AuthenticationFlowCoordinatorDelegate: AnyObject {
|
||||
func authenticationFlowCoordinator(didLoginWithSession userSession: UserSessionProtocol)
|
||||
}
|
||||
|
||||
class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||
private let appLockService: AppLockServiceProtocol
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
private let appSettings: AppSettings
|
||||
private let analytics: AnalyticsService
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var oidcPresenter: OIDCAuthenticationPresenter?
|
||||
|
||||
// periphery: ignore - used to store the coordinator to avoid deallocation
|
||||
private var appLockFlowCoordinator: AppLockSetupFlowCoordinator?
|
||||
|
||||
// periphery:ignore - retaining purpose
|
||||
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
|
||||
|
||||
weak var delegate: AuthenticationFlowCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
navigationRootCoordinator: NavigationRootCoordinator,
|
||||
appSettings: AppSettings,
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.appLockService = appLockService
|
||||
self.bugReportService = bugReportService
|
||||
self.navigationRootCoordinator = navigationRootCoordinator
|
||||
self.appSettings = appSettings
|
||||
self.analytics = analytics
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
}
|
||||
|
||||
func start() {
|
||||
showStartScreen()
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func handleOIDCRedirectURL(_ url: URL) {
|
||||
guard let oidcPresenter else {
|
||||
MXLog.error("Failed to find an OIDC request in progress.")
|
||||
return
|
||||
}
|
||||
|
||||
oidcPresenter.handleUniversalLinkCallback(url)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func showStartScreen() {
|
||||
let coordinator = AuthenticationStartScreenCoordinator()
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .login:
|
||||
Task { await self.startAuthentication() }
|
||||
case .reportProblem:
|
||||
showReportProblemScreen()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(navigationStackCoordinator)
|
||||
}
|
||||
|
||||
private func showReportProblemScreen() {
|
||||
bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(navigationStackCoordinator),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
bugReportService: bugReportService,
|
||||
userID: nil,
|
||||
deviceID: nil))
|
||||
bugReportFlowCoordinator?.start()
|
||||
}
|
||||
|
||||
private func startAuthentication() async {
|
||||
startLoading()
|
||||
|
||||
switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) {
|
||||
case .success:
|
||||
stopLoading()
|
||||
showServerConfirmationScreen()
|
||||
case .failure:
|
||||
stopLoading()
|
||||
showServerSelectionScreen(isModallyPresented: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func showServerSelectionScreen(isModallyPresented: Bool) {
|
||||
let navigationCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
userIndicatorController: userIndicatorController,
|
||||
isModallyPresented: isModallyPresented)
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .updated:
|
||||
if isModallyPresented {
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
} else {
|
||||
// We are here because the default server failed to respond.
|
||||
if authenticationService.homeserver.value.loginMode == .password {
|
||||
// Add the password login screen directly to the flow, its fine.
|
||||
showLoginScreen()
|
||||
} else {
|
||||
// OIDC is presented from the confirmation screen so replace the
|
||||
// server selection screen which was inserted to handle the failure.
|
||||
navigationStackCoordinator.pop()
|
||||
showServerConfirmationScreen()
|
||||
}
|
||||
}
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if isModallyPresented {
|
||||
navigationCoordinator.setRootCoordinator(coordinator)
|
||||
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
|
||||
} else {
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
private func showServerConfirmationScreen() {
|
||||
let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
authenticationFlow: .login)
|
||||
let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .continue(let window):
|
||||
if authenticationService.homeserver.value.loginMode == .oidc, let window {
|
||||
showOIDCAuthentication(presentationAnchor: window)
|
||||
} else {
|
||||
showLoginScreen()
|
||||
}
|
||||
case .changeServer:
|
||||
showServerSelectionScreen(isModallyPresented: true)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func showOIDCAuthentication(presentationAnchor: UIWindow) {
|
||||
startLoading()
|
||||
|
||||
Task {
|
||||
switch await authenticationService.urlForOIDCLogin() {
|
||||
case .failure(let error):
|
||||
stopLoading()
|
||||
handleError(error)
|
||||
case .success(let oidcData):
|
||||
stopLoading()
|
||||
|
||||
let presenter = OIDCAuthenticationPresenter(authenticationService: authenticationService,
|
||||
oidcRedirectURL: appSettings.oidcRedirectURL,
|
||||
presentationAnchor: presentationAnchor)
|
||||
self.oidcPresenter = presenter
|
||||
switch await presenter.authenticate(using: oidcData) {
|
||||
case .success(let userSession):
|
||||
userHasSignedIn(userSession: userSession)
|
||||
case .failure(let error):
|
||||
handleError(error)
|
||||
}
|
||||
oidcPresenter = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showLoginScreen() {
|
||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
analytics: analytics,
|
||||
userIndicatorController: userIndicatorController)
|
||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .signedIn(let userSession):
|
||||
userHasSignedIn(userSession: userSession)
|
||||
case .configuredForOIDC:
|
||||
// Pop back to the confirmation screen for OIDC login to continue.
|
||||
navigationStackCoordinator.pop(animated: false)
|
||||
case .isOnWaitlist(let credentials):
|
||||
showWaitlistScreen(for: credentials)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func showWaitlistScreen(for credentials: WaitlistScreenCredentials) {
|
||||
let parameters = WaitlistScreenCoordinatorParameters(credentials: credentials,
|
||||
authenticationService: authenticationService)
|
||||
let coordinator = WaitlistScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .signedIn(let userSession):
|
||||
userHasSignedIn(userSession: userSession)
|
||||
case .cancel:
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func userHasSignedIn(userSession: UserSessionProtocol) {
|
||||
appSettings.lastLoginDate = .now
|
||||
|
||||
if appSettings.appLockIsMandatory, !appLockService.isEnabled {
|
||||
showAppLockSetupFlow(userSession: userSession)
|
||||
} else if analytics.shouldShowAnalyticsPrompt {
|
||||
showAnalyticsPrompt(userSession: userSession)
|
||||
} else {
|
||||
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
|
||||
private func showAppLockSetupFlow(userSession: UserSessionProtocol) {
|
||||
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .onboarding,
|
||||
appLockService: appLockService,
|
||||
navigationStackCoordinator: navigationStackCoordinator)
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .complete:
|
||||
appLockFlowCoordinator = nil
|
||||
if analytics.shouldShowAnalyticsPrompt {
|
||||
showAnalyticsPrompt(userSession: userSession)
|
||||
} else {
|
||||
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
||||
}
|
||||
case .forceLogout:
|
||||
fatalError("The PIN creation flow should not fail.")
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
appLockFlowCoordinator = coordinator
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func showAnalyticsPrompt(userSession: UserSessionProtocol) {
|
||||
let coordinator = AnalyticsPromptScreenCoordinator(analytics: analytics, termsURL: appSettings.analyticsConfiguration.termsURL)
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .done:
|
||||
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "authenticationFlowCoordinatorLoading"
|
||||
|
||||
private func startLoading() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func stopLoading() {
|
||||
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
MXLog.warning("Error occurred: \(error)")
|
||||
|
||||
switch error {
|
||||
case .oidcError(.notSupported):
|
||||
// Temporary alert hijacking the use of .notSupported, can be removed when OIDC support is in the SDK.
|
||||
userIndicatorController.alertInfo = AlertInfo(id: UUID(),
|
||||
title: L10n.commonError,
|
||||
message: L10n.commonServerNotSupported)
|
||||
case .oidcError(.userCancellation):
|
||||
// No need to show an error, the user cancelled authentication.
|
||||
break
|
||||
default:
|
||||
userIndicatorController.alertInfo = AlertInfo(id: UUID())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .blockedUsers:
|
||||
presentBlockedUsersScreen()
|
||||
case .sessionVerification:
|
||||
presentSessionVerificationScreen()
|
||||
Task {
|
||||
await self.presentSessionVerificationScreen()
|
||||
}
|
||||
case .accountSessions:
|
||||
presentAccountSessionsListURL()
|
||||
case .notifications:
|
||||
@@ -211,8 +213,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentSessionVerificationScreen() {
|
||||
guard let sessionVerificationController = parameters.userSession.sessionVerificationController else {
|
||||
private func presentSessionVerificationScreen() async {
|
||||
guard case let .success(sessionVerificationController) = await parameters.userSession.clientProxy.sessionVerificationControllerProxy() else {
|
||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ enum UserSessionFlowCoordinatorAction {
|
||||
|
||||
class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
private let windowManager: WindowManagerProtocol
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
@@ -56,7 +57,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
navigationSplitCoordinator: NavigationSplitCoordinator,
|
||||
navigationRootCoordinator: NavigationRootCoordinator,
|
||||
windowManager: WindowManagerProtocol,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
@@ -65,11 +66,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
analytics: AnalyticsService) {
|
||||
stateMachine = UserSessionFlowCoordinatorStateMachine()
|
||||
self.userSession = userSession
|
||||
self.navigationSplitCoordinator = navigationSplitCoordinator
|
||||
self.navigationRootCoordinator = navigationRootCoordinator
|
||||
self.windowManager = windowManager
|
||||
self.bugReportService = bugReportService
|
||||
self.appSettings = appSettings
|
||||
|
||||
navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
|
||||
|
||||
sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
|
||||
@@ -139,12 +142,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
func start() {
|
||||
if !appSettings.hasShownWelcomeScreen {
|
||||
stateMachine.processEvent(.startWithWelcomeScreen)
|
||||
} else {
|
||||
// Otherwise go straight to the home screen.
|
||||
stateMachine.processEvent(.start)
|
||||
}
|
||||
stateMachine.processEvent(.start)
|
||||
}
|
||||
|
||||
func stop() { }
|
||||
@@ -218,14 +216,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .start, .roomList):
|
||||
presentHomeScreen()
|
||||
|
||||
case (.initial, .startWithWelcomeScreen, .welcomeScreen):
|
||||
presentHomeScreen()
|
||||
presentWelcomeScreen()
|
||||
case (.roomList, .presentWelcomeScreen, .welcomeScreen):
|
||||
presentWelcomeScreen()
|
||||
case (.welcomeScreen, .dismissedWelcomeScreen, .roomList):
|
||||
break
|
||||
|
||||
case(.roomList, .selectRoom, .roomList):
|
||||
break
|
||||
@@ -238,7 +228,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.roomList, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
presentSessionVerification(animated: animated)
|
||||
Task {
|
||||
await self.presentSessionVerification(animated: animated)
|
||||
}
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .roomList):
|
||||
break
|
||||
|
||||
@@ -341,21 +333,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentWelcomeScreen() {
|
||||
let welcomeScreenCoordinator = WelcomeScreenScreenCoordinator()
|
||||
welcomeScreenCoordinator.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(welcomeScreenCoordinator) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedWelcomeScreen)
|
||||
}
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator)
|
||||
}
|
||||
|
||||
private func runLogoutFlow() async {
|
||||
@@ -405,8 +384,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// MARK: Session verification
|
||||
|
||||
private func presentSessionVerification(animated: Bool) {
|
||||
guard let sessionVerificationController = userSession.sessionVerificationController else {
|
||||
private func presentSessionVerification(animated: Bool) async {
|
||||
guard case let .success(sessionVerificationController) = await userSession.clientProxy.sessionVerificationControllerProxy() else {
|
||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,7 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
enum State: StateType {
|
||||
/// The initial state, used before the coordinator starts
|
||||
case initial
|
||||
|
||||
/// Showing the welcome screen.
|
||||
case welcomeScreen
|
||||
|
||||
|
||||
/// Showing the home screen. The `selectedRoomID` represents the timeline shown on the detail panel (if any)
|
||||
case roomList(selectedRoomID: String?)
|
||||
|
||||
@@ -56,15 +53,6 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
enum Event: EventType {
|
||||
/// Start the user session flows.
|
||||
case start
|
||||
/// Starts the user session flows with the welcome screen.
|
||||
/// **Note:** This is event is only for users who used the app before v1.1.8.
|
||||
/// It can be removed once the older TestFlight builds have expired.
|
||||
case startWithWelcomeScreen
|
||||
|
||||
/// Request presentation of the welcome screen.
|
||||
case presentWelcomeScreen
|
||||
/// The welcome screen has been dismissed.
|
||||
case dismissedWelcomeScreen
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomID:the room identifier
|
||||
@@ -116,8 +104,6 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
private func configure() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomID: nil)])
|
||||
stateMachine.addRoutes(event: .startWithWelcomeScreen, transitions: [.initial => .welcomeScreen])
|
||||
stateMachine.addRoutes(event: .dismissedWelcomeScreen, transitions: [.welcomeScreen => .roomList(selectedRoomID: nil)])
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
switch (fromState, event) {
|
||||
@@ -157,9 +143,6 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
case (.invitesScreen(let selectedRoomID), .dismissedInvitesScreen):
|
||||
return .roomList(selectedRoomID: selectedRoomID)
|
||||
|
||||
case (.roomList, .presentWelcomeScreen):
|
||||
return .welcomeScreen
|
||||
|
||||
case (.roomList(let selectedRoomID), .showLogoutConfirmationScreen):
|
||||
return .logoutConfirmationScreen(selectedRoomID: selectedRoomID)
|
||||
|
||||
Reference in New Issue
Block a user