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:
@@ -1,340 +0,0 @@
|
||||
//
|
||||
// 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 AuthenticationCoordinatorDelegate: AnyObject {
|
||||
func authenticationCoordinator(didLoginWithSession userSession: UserSessionProtocol)
|
||||
}
|
||||
|
||||
class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||
private let appLockService: AppLockServiceProtocol
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
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: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
navigationStackCoordinator: NavigationStackCoordinator,
|
||||
appSettings: AppSettings,
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.appLockService = appLockService
|
||||
self.bugReportService = bugReportService
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
self.appSettings = appSettings
|
||||
self.analytics = analytics
|
||||
self.userIndicatorController = userIndicatorController
|
||||
}
|
||||
|
||||
func start() {
|
||||
showOnboarding()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
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 showOnboarding() {
|
||||
let coordinator = OnboardingScreenCoordinator()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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?.authenticationCoordinator(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?.authenticationCoordinator(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?.authenticationCoordinator(didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,7 @@ enum UIConstants {
|
||||
static let actionButtonBottomPadding: CGFloat = 24
|
||||
|
||||
/// The padding used to the top of the view for breaker screens that don't have a navigation bar.
|
||||
static let onboardingBreakerScreenTopPadding: CGFloat = 80
|
||||
|
||||
static let welcomeScreenTopPadding: CGFloat = 122
|
||||
static let startScreenBreakerScreenTopPadding: CGFloat = 80
|
||||
|
||||
/// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`.
|
||||
static func spacerHeight(in geometry: GeometryProxy) -> CGFloat {
|
||||
|
||||
@@ -27,13 +27,13 @@ struct WaitingDialog<Content: View, BottomContent: View>: View {
|
||||
private var contentTopPadding: CGFloat {
|
||||
if verticalSizeClass == .compact {
|
||||
// Reduced value for iPhones in landscape.
|
||||
return UIConstants.onboardingBreakerScreenTopPadding
|
||||
return UIConstants.startScreenBreakerScreenTopPadding
|
||||
} else if horizontalSizeClass == .compact {
|
||||
// The default value for portrait iPhones.
|
||||
return 2 * UIConstants.onboardingBreakerScreenTopPadding
|
||||
return 2 * UIConstants.startScreenBreakerScreenTopPadding
|
||||
} else {
|
||||
// Larger on iPad specifically for 11" in Landscape.
|
||||
return 2.7 * UIConstants.onboardingBreakerScreenTopPadding
|
||||
return 2.7 * UIConstants.startScreenBreakerScreenTopPadding
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ struct WaitingDialog<Content: View, BottomContent: View>: View {
|
||||
@ViewBuilder var bottomContent: () -> BottomContent
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: UIConstants.onboardingBreakerScreenTopPadding, horizontalPadding: 0) {
|
||||
FullscreenDialog(topPadding: UIConstants.startScreenBreakerScreenTopPadding, horizontalPadding: 0) {
|
||||
content()
|
||||
.padding(.top, contentTopPadding)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
@@ -68,7 +68,7 @@ struct WaitingDialog<Content: View, BottomContent: View>: View {
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: -5) {
|
||||
Color.white
|
||||
.frame(maxHeight: UIConstants.onboardingBreakerScreenTopPadding)
|
||||
.frame(maxHeight: UIConstants.startScreenBreakerScreenTopPadding)
|
||||
|
||||
Image(asset: Asset.Images.waitingGradient)
|
||||
.resizable()
|
||||
|
||||
Reference in New Issue
Block a user