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:
Stefan Ceriu
2024-03-13 11:24:48 +02:00
committed by GitHub
parent d7e0bbc537
commit 5944550721
56 changed files with 277 additions and 754 deletions
@@ -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)