mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Update OIDC presentation (#987)
* Present OIDC directly on the ServerConfirmationScreen. Read the actual window from the view. * Pop back to the confirmation screen when a user enters an MXID that needs OIDC. * Remove OIDC error handling from the login screen.
This commit is contained in:
@@ -26,15 +26,18 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
|
|||||||
class AuthenticationCoordinator: CoordinatorProtocol {
|
class AuthenticationCoordinator: CoordinatorProtocol {
|
||||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||||
|
private let serviceLocator: ServiceLocator
|
||||||
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||||
|
|
||||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||||
navigationStackCoordinator: NavigationStackCoordinator) {
|
navigationStackCoordinator: NavigationStackCoordinator,
|
||||||
|
serviceLocator: ServiceLocator = .shared) {
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
self.navigationStackCoordinator = navigationStackCoordinator
|
self.navigationStackCoordinator = navigationStackCoordinator
|
||||||
|
self.serviceLocator = serviceLocator
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
@@ -64,7 +67,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
private func startAuthentication() async {
|
private func startAuthentication() async {
|
||||||
startLoading()
|
startLoading()
|
||||||
|
|
||||||
switch await authenticationService.configure(for: ServiceLocator.shared.settings.defaultHomeserverAddress) {
|
switch await authenticationService.configure(for: serviceLocator.settings.defaultHomeserverAddress) {
|
||||||
case .success:
|
case .success:
|
||||||
stopLoading()
|
stopLoading()
|
||||||
showServerConfirmationScreen()
|
showServerConfirmationScreen()
|
||||||
@@ -76,7 +79,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
private func showServerSelectionScreen(isModallyPresented: Bool) {
|
private func showServerSelectionScreen(isModallyPresented: Bool) {
|
||||||
let navigationCoordinator = NavigationStackCoordinator()
|
let navigationCoordinator = NavigationStackCoordinator()
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol! = isModallyPresented ? UserIndicatorController(rootCoordinator: navigationCoordinator) : ServiceLocator.shared.userIndicatorController
|
let userIndicatorController: UserIndicatorControllerProtocol! = isModallyPresented ? UserIndicatorController(rootCoordinator: navigationCoordinator) : serviceLocator.userIndicatorController
|
||||||
|
|
||||||
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||||
userIndicatorController: userIndicatorController,
|
userIndicatorController: userIndicatorController,
|
||||||
@@ -115,10 +118,14 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .confirm:
|
case .continue(let window):
|
||||||
self.showLoginScreen()
|
if authenticationService.homeserver.value.loginMode == .oidc, let window {
|
||||||
|
showOIDCAuthentication(presentationAnchor: window)
|
||||||
|
} else {
|
||||||
|
showLoginScreen()
|
||||||
|
}
|
||||||
case .changeServer:
|
case .changeServer:
|
||||||
self.showServerSelectionScreen(isModallyPresented: true)
|
showServerSelectionScreen(isModallyPresented: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -126,6 +133,28 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
navigationStackCoordinator.push(coordinator)
|
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, presentationAnchor: presentationAnchor)
|
||||||
|
switch await presenter.authenticate(using: oidcData) {
|
||||||
|
case .success(let userSession):
|
||||||
|
userHasSignedIn(userSession: userSession)
|
||||||
|
case .failure(let error):
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func showLoginScreen() {
|
private func showLoginScreen() {
|
||||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService)
|
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService)
|
||||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||||
@@ -135,7 +164,10 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .signedIn(let userSession):
|
case .signedIn(let userSession):
|
||||||
self.userHasSignedIn(userSession: userSession)
|
userHasSignedIn(userSession: userSession)
|
||||||
|
case .configuredForOIDC:
|
||||||
|
// Pop back to the confirmation screen for OIDC login to continue.
|
||||||
|
navigationStackCoordinator.pop(animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +182,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func showAnalyticsPromptIfNeeded(completion: @escaping () -> Void) {
|
private func showAnalyticsPromptIfNeeded(completion: @escaping () -> Void) {
|
||||||
guard ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt else {
|
guard serviceLocator.analytics.shouldShowAnalyticsPrompt else {
|
||||||
completion()
|
completion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -164,13 +196,31 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
private static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
private static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||||
|
|
||||||
private func startLoading() {
|
private func startLoading() {
|
||||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
serviceLocator.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
type: .modal,
|
type: .modal,
|
||||||
title: L10n.commonLoading,
|
title: L10n.commonLoading,
|
||||||
persistent: true))
|
persistent: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopLoading() {
|
private func stopLoading() {
|
||||||
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
serviceLocator.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.
|
||||||
|
serviceLocator.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:
|
||||||
|
serviceLocator.userIndicatorController.alertInfo = AlertInfo(id: UUID())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ struct LoginScreenCoordinatorParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum LoginScreenCoordinatorAction {
|
enum LoginScreenCoordinatorAction {
|
||||||
|
/// The homeserver was updated to one that supports OIDC.
|
||||||
|
case configuredForOIDC
|
||||||
/// Login was successful.
|
/// Login was successful.
|
||||||
case signedIn(UserSessionProtocol)
|
case signedIn(UserSessionProtocol)
|
||||||
}
|
}
|
||||||
@@ -33,7 +35,6 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
@CancellableTask private var currentTask: Task<Void, Error>?
|
@CancellableTask private var currentTask: Task<Void, Error>?
|
||||||
|
|
||||||
private let oidcAuthenticationPresenter: OIDCAuthenticationPresenter
|
|
||||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||||
|
|
||||||
var callback: (@MainActor (LoginScreenCoordinatorAction) -> Void)?
|
var callback: (@MainActor (LoginScreenCoordinatorAction) -> Void)?
|
||||||
@@ -44,8 +45,6 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver.value)
|
viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver.value)
|
||||||
|
|
||||||
oidcAuthenticationPresenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -56,13 +55,11 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .parseUsername(let username):
|
case .parseUsername(let username):
|
||||||
self.parseUsername(username)
|
parseUsername(username)
|
||||||
case .forgotPassword:
|
case .forgotPassword:
|
||||||
self.showForgotPasswordScreen()
|
showForgotPasswordScreen()
|
||||||
case .login(let username, let password):
|
case .login(let username, let password):
|
||||||
self.login(username: username, password: password)
|
login(username: username, password: password)
|
||||||
case .continueWithOIDC:
|
|
||||||
self.continueWithOIDC()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,38 +111,11 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount))
|
viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount))
|
||||||
case .slidingSyncNotAvailable:
|
case .slidingSyncNotAvailable:
|
||||||
viewModel.displayError(.slidingSyncAlert)
|
viewModel.displayError(.slidingSyncAlert)
|
||||||
case .oidcError(.notSupported):
|
|
||||||
// Temporary alert hijacking the use of .notSupported, can be removed when OIDC support is in the SDK.
|
|
||||||
viewModel.displayError(.alert(L10n.commonServerNotSupported))
|
|
||||||
case .oidcError(.userCancellation):
|
|
||||||
// No need to show an error, the user cancelled authentication.
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
viewModel.displayError(.alert(L10n.errorUnknown))
|
viewModel.displayError(.alert(L10n.errorUnknown))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func continueWithOIDC() {
|
|
||||||
startLoading(isInteractionBlocking: true)
|
|
||||||
|
|
||||||
Task {
|
|
||||||
switch await authenticationService.urlForOIDCLogin() {
|
|
||||||
case .failure(let error):
|
|
||||||
stopLoading()
|
|
||||||
handleError(error)
|
|
||||||
case .success(let oidcData):
|
|
||||||
stopLoading()
|
|
||||||
|
|
||||||
switch await oidcAuthenticationPresenter.authenticate(using: oidcData) {
|
|
||||||
case .success(let userSession):
|
|
||||||
callback?(.signedIn(userSession))
|
|
||||||
case .failure(let error):
|
|
||||||
handleError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Requests the authentication coordinator to log in using the specified credentials.
|
/// Requests the authentication coordinator to log in using the specified credentials.
|
||||||
private func login(username: String, password: String) {
|
private func login(username: String, password: String) {
|
||||||
MXLog.info("Starting login with password.")
|
MXLog.info("Starting login with password.")
|
||||||
@@ -177,8 +147,12 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
Task {
|
Task {
|
||||||
switch await authenticationService.configure(for: homeserverDomain) {
|
switch await authenticationService.configure(for: homeserverDomain) {
|
||||||
case .success:
|
case .success:
|
||||||
updateViewModel()
|
|
||||||
stopLoading()
|
stopLoading()
|
||||||
|
if authenticationService.homeserver.value.loginMode == .oidc {
|
||||||
|
callback?(.configuredForOIDC)
|
||||||
|
} else {
|
||||||
|
updateViewModel()
|
||||||
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
stopLoading()
|
stopLoading()
|
||||||
handleError(error)
|
handleError(error)
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ enum LoginScreenViewModelAction: CustomStringConvertible {
|
|||||||
case forgotPassword
|
case forgotPassword
|
||||||
/// Login using the supplied credentials.
|
/// Login using the supplied credentials.
|
||||||
case login(username: String, password: String)
|
case login(username: String, password: String)
|
||||||
/// Continue using OIDC.
|
|
||||||
case continueWithOIDC
|
|
||||||
|
|
||||||
/// A string representation of the action, ignoring any associated values that could leak PII.
|
/// A string representation of the action, ignoring any associated values that could leak PII.
|
||||||
var description: String {
|
var description: String {
|
||||||
@@ -35,8 +33,6 @@ enum LoginScreenViewModelAction: CustomStringConvertible {
|
|||||||
return "forgotPassword"
|
return "forgotPassword"
|
||||||
case .login:
|
case .login:
|
||||||
return "login"
|
return "login"
|
||||||
case .continueWithOIDC:
|
|
||||||
return "continueWithOIDC"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,8 +75,6 @@ enum LoginScreenViewAction {
|
|||||||
case forgotPassword
|
case forgotPassword
|
||||||
/// Continue using the input username and password.
|
/// Continue using the input username and password.
|
||||||
case next
|
case next
|
||||||
/// Continue using OIDC.
|
|
||||||
case continueWithOIDC
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LoginScreenErrorType: Hashable {
|
enum LoginScreenErrorType: Hashable {
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
|||||||
callback?(.forgotPassword)
|
callback?(.forgotPassword)
|
||||||
case .next:
|
case .next:
|
||||||
callback?(.login(username: state.bindings.username, password: state.bindings.password))
|
callback?(.login(username: state.bindings.username, password: state.bindings.password))
|
||||||
case .continueWithOIDC:
|
|
||||||
callback?(.continueWithOIDC)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ struct LoginScreen: View {
|
|||||||
case .password:
|
case .password:
|
||||||
loginForm
|
loginForm
|
||||||
case .oidc:
|
case .oidc:
|
||||||
oidcButton
|
// This should never be shown.
|
||||||
|
ProgressView()
|
||||||
default:
|
default:
|
||||||
loginUnavailableText
|
loginUnavailableText
|
||||||
}
|
}
|
||||||
@@ -106,15 +107,6 @@ struct LoginScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The OIDC button that can be used for login.
|
|
||||||
var oidcButton: some View {
|
|
||||||
Button { context.send(viewAction: .continueWithOIDC) } label: {
|
|
||||||
Text(L10n.actionContinue)
|
|
||||||
}
|
|
||||||
.buttonStyle(.elementAction(.xLarge))
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.loginScreen.oidc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Text shown if neither password or OIDC login is supported.
|
/// Text shown if neither password or OIDC login is supported.
|
||||||
var loginUnavailableText: some View {
|
var loginUnavailableText: some View {
|
||||||
Text(L10n.screenLoginErrorUnsupportedAuthentication)
|
Text(L10n.screenLoginErrorUnsupportedAuthentication)
|
||||||
@@ -155,10 +147,10 @@ struct LoginScreen_Previews: PreviewProvider {
|
|||||||
.previewDisplayName("matrix.org")
|
.previewDisplayName("matrix.org")
|
||||||
screen(for: credentialsViewModel)
|
screen(for: credentialsViewModel)
|
||||||
.previewDisplayName("Credentials Entered")
|
.previewDisplayName("Credentials Entered")
|
||||||
screen(for: LoginScreenViewModel(homeserver: .mockOIDC))
|
|
||||||
.previewDisplayName("OIDC")
|
|
||||||
screen(for: LoginScreenViewModel(homeserver: .mockUnsupported))
|
screen(for: LoginScreenViewModel(homeserver: .mockUnsupported))
|
||||||
.previewDisplayName("Unsupported")
|
.previewDisplayName("Unsupported")
|
||||||
|
screen(for: LoginScreenViewModel(homeserver: .mockOIDC))
|
||||||
|
.previewDisplayName("OIDC Fallback")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func screen(for viewModel: LoginScreenViewModel) -> some View {
|
static func screen(for viewModel: LoginScreenViewModel) -> some View {
|
||||||
|
|||||||
@@ -17,12 +17,15 @@
|
|||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
|
|
||||||
/// Presents a web authentication session for an OIDC request.
|
/// Presents a web authentication session for an OIDC request.
|
||||||
class OIDCAuthenticationPresenter {
|
@MainActor
|
||||||
|
class OIDCAuthenticationPresenter: NSObject {
|
||||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||||
private let oidcPresentationContent = OIDCPresentationContext()
|
private let presentationAnchor: UIWindow
|
||||||
|
|
||||||
init(authenticationService: AuthenticationServiceProxyProtocol) {
|
init(authenticationService: AuthenticationServiceProxyProtocol, presentationAnchor: UIWindow) {
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
|
self.presentationAnchor = presentationAnchor
|
||||||
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Presents a web authentication session for the supplied data.
|
/// Presents a web authentication session for the supplied data.
|
||||||
@@ -48,7 +51,7 @@ class OIDCAuthenticationPresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.prefersEphemeralWebBrowserSession = false
|
session.prefersEphemeralWebBrowserSession = false
|
||||||
session.presentationContextProvider = oidcPresentationContent
|
session.presentationContextProvider = self
|
||||||
session.start()
|
session.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,11 +70,8 @@ class OIDCAuthenticationPresenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OIDCPresentationContext: NSObject, ASWebAuthenticationPresentationContextProviding {
|
// MARK: ASWebAuthenticationPresentationContextProviding
|
||||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
||||||
guard let window = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first?.keyWindow else {
|
extension OIDCAuthenticationPresenter: ASWebAuthenticationPresentationContextProviding {
|
||||||
fatalError("Failed to find the main window.")
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { presentationAnchor }
|
||||||
}
|
|
||||||
return window
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,7 @@ struct ServerConfirmationScreenCoordinatorParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ServerConfirmationScreenCoordinatorAction {
|
enum ServerConfirmationScreenCoordinatorAction {
|
||||||
case confirm
|
case `continue`(UIWindow?)
|
||||||
case changeServer
|
case changeServer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .confirm:
|
case .confirm:
|
||||||
self.actionsSubject.send(.confirm)
|
self.actionsSubject.send(.continue(viewModel.context.viewState.window))
|
||||||
case .changeServer:
|
case .changeServer:
|
||||||
self.actionsSubject.send(.changeServer)
|
self.actionsSubject.send(.changeServer)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -14,7 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import SwiftUI
|
||||||
|
|
||||||
enum ServerConfirmationScreenViewModelAction {
|
enum ServerConfirmationScreenViewModelAction {
|
||||||
/// The user would like to continue with the current homeserver.
|
/// The user would like to continue with the current homeserver.
|
||||||
@@ -28,6 +28,8 @@ struct ServerConfirmationScreenViewState: BindableState {
|
|||||||
var homeserverAddress: String
|
var homeserverAddress: String
|
||||||
/// The flow being attempted on the selected homeserver.
|
/// The flow being attempted on the selected homeserver.
|
||||||
let authenticationFlow: AuthenticationFlow
|
let authenticationFlow: AuthenticationFlow
|
||||||
|
/// The presentation anchor used for OIDC authentication.
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
/// The screen's title.
|
/// The screen's title.
|
||||||
var title: String {
|
var title: String {
|
||||||
@@ -57,6 +59,8 @@ struct ServerConfirmationScreenViewState: BindableState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ServerConfirmationScreenViewAction {
|
enum ServerConfirmationScreenViewAction {
|
||||||
|
/// Updates the window used as the OIDC presentation anchor.
|
||||||
|
case updateWindow(UIWindow)
|
||||||
/// The user would like to continue with the current homeserver.
|
/// The user would like to continue with the current homeserver.
|
||||||
case confirm
|
case confirm
|
||||||
/// The user would like to change to a different homeserver.
|
/// The user would like to change to a different homeserver.
|
||||||
|
|||||||
+3
@@ -43,6 +43,9 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
|
|
||||||
override func process(viewAction: ServerConfirmationScreenViewAction) {
|
override func process(viewAction: ServerConfirmationScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
|
case .updateWindow(let window):
|
||||||
|
guard state.window != window else { return }
|
||||||
|
Task { state.window = window }
|
||||||
case .confirm:
|
case .confirm:
|
||||||
actionsSubject.send(.confirm)
|
actionsSubject.send(.confirm)
|
||||||
case .changeServer:
|
case .changeServer:
|
||||||
|
|||||||
+4
@@ -34,6 +34,10 @@ struct ServerConfirmationScreen: View {
|
|||||||
.readableFrame()
|
.readableFrame()
|
||||||
.background(Color.element.background.ignoresSafeArea())
|
.background(Color.element.background.ignoresSafeArea())
|
||||||
}
|
}
|
||||||
|
.introspectViewController { viewController in
|
||||||
|
guard let window = viewController.view.window else { return }
|
||||||
|
context.send(viewAction: .updateWindow(window))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main content of the view to be shown in a scroll view.
|
/// The main content of the view to be shown in a scroll view.
|
||||||
|
|||||||
+6
-6
@@ -43,7 +43,6 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
private let parameters: SoftLogoutScreenCoordinatorParameters
|
private let parameters: SoftLogoutScreenCoordinatorParameters
|
||||||
private var viewModel: SoftLogoutScreenViewModelProtocol
|
private var viewModel: SoftLogoutScreenViewModelProtocol
|
||||||
|
|
||||||
private let oidcAuthenticationPresenter: OIDCAuthenticationPresenter
|
|
||||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||||
|
|
||||||
var callback: (@MainActor (SoftLogoutScreenCoordinatorResult) -> Void)?
|
var callback: (@MainActor (SoftLogoutScreenCoordinatorResult) -> Void)?
|
||||||
@@ -55,8 +54,6 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel = SoftLogoutScreenViewModel(credentials: parameters.credentials,
|
viewModel = SoftLogoutScreenViewModel(credentials: parameters.credentials,
|
||||||
homeserver: homeserver.value,
|
homeserver: homeserver.value,
|
||||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||||
|
|
||||||
oidcAuthenticationPresenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -74,7 +71,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .clearAllData:
|
case .clearAllData:
|
||||||
self.callback?(.clearAllData)
|
self.callback?(.clearAllData)
|
||||||
case .continueWithOIDC:
|
case .continueWithOIDC:
|
||||||
self.continueWithOIDC()
|
self.continueWithOIDC(presentationAnchor: viewModel.context.viewState.window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +127,9 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func continueWithOIDC() {
|
private func continueWithOIDC(presentationAnchor: UIWindow?) {
|
||||||
|
guard let presentationAnchor else { return }
|
||||||
|
|
||||||
startLoading()
|
startLoading()
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
@@ -141,7 +140,8 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .success(let oidcData):
|
case .success(let oidcData):
|
||||||
stopLoading()
|
stopLoading()
|
||||||
|
|
||||||
switch await oidcAuthenticationPresenter.authenticate(using: oidcData) {
|
let presenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService, presentationAnchor: presentationAnchor)
|
||||||
|
switch await presenter.authenticate(using: oidcData) {
|
||||||
case .success(let userSession):
|
case .success(let userSession):
|
||||||
callback?(.signedIn(userSession))
|
callback?(.signedIn(userSession))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ struct SoftLogoutScreenViewState: BindableState {
|
|||||||
/// The types of login supported by the homeserver.
|
/// The types of login supported by the homeserver.
|
||||||
var loginMode: LoginMode { homeserver.loginMode }
|
var loginMode: LoginMode { homeserver.loginMode }
|
||||||
|
|
||||||
|
/// The presentation anchor used for OIDC authentication.
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
/// Whether to show recover encryption keys message
|
/// Whether to show recover encryption keys message
|
||||||
var showRecoverEncryptionKeysMessage: Bool {
|
var showRecoverEncryptionKeysMessage: Bool {
|
||||||
keyBackupNeeded
|
keyBackupNeeded
|
||||||
@@ -83,6 +86,8 @@ struct SoftLogoutScreenBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum SoftLogoutScreenViewAction {
|
enum SoftLogoutScreenViewAction {
|
||||||
|
/// Updates the window used as the OIDC presentation anchor.
|
||||||
|
case updateWindow(UIWindow?)
|
||||||
/// Login.
|
/// Login.
|
||||||
case login
|
case login
|
||||||
/// Forgot password
|
/// Forgot password
|
||||||
|
|||||||
+3
@@ -43,6 +43,9 @@ class SoftLogoutScreenViewModel: SoftLogoutScreenViewModelType, SoftLogoutScreen
|
|||||||
callback?(.clearAllData)
|
callback?(.clearAllData)
|
||||||
case .continueWithOIDC:
|
case .continueWithOIDC:
|
||||||
callback?(.continueWithOIDC)
|
callback?(.continueWithOIDC)
|
||||||
|
case .updateWindow(let window):
|
||||||
|
guard state.window != window else { return }
|
||||||
|
Task { state.window = window }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ struct SoftLogoutScreen: View {
|
|||||||
}
|
}
|
||||||
.background(Color.element.background.ignoresSafeArea())
|
.background(Color.element.background.ignoresSafeArea())
|
||||||
.alert(item: $context.alertInfo) { $0.alert }
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
|
.introspectViewController { viewController in
|
||||||
|
guard let window = viewController.view.window else { return }
|
||||||
|
context.send(viewAction: .updateWindow(window))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The title, message and icon at the top of the screen.
|
/// The title, message and icon at the top of the screen.
|
||||||
|
|||||||
@@ -32,17 +32,6 @@ class LoginScreenUITests: XCTestCase {
|
|||||||
try await app.assertScreenshot(.login, step: 0)
|
try await app.assertScreenshot(.login, step: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOIDC() async throws {
|
|
||||||
// Given the initial login screen.
|
|
||||||
let app = Application.launch(.login)
|
|
||||||
|
|
||||||
// When entering a username on a homeserver that only supports OIDC.
|
|
||||||
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:company.com\n")
|
|
||||||
|
|
||||||
// Then the screen should be configured for OIDC.
|
|
||||||
try await app.assertScreenshot(.login, step: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUnsupported() async throws {
|
func testUnsupported() async throws {
|
||||||
// Given the initial login screen.
|
// Given the initial login screen.
|
||||||
let app = Application.launch(.login)
|
let app = Application.launch(.login)
|
||||||
@@ -51,6 +40,6 @@ class LoginScreenUITests: XCTestCase {
|
|||||||
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n")
|
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n")
|
||||||
|
|
||||||
// Then the screen should not allow login to continue.
|
// Then the screen should not allow login to continue.
|
||||||
try await app.assertScreenshot(.login, step: 2)
|
try await app.assertScreenshot(.login, step: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ServerSelectionUITests: XCTestCase {
|
|||||||
|
|
||||||
// Then an error should be shown and the confirmation button disabled.
|
// Then an error should be shown and the confirmation button disabled.
|
||||||
try await app.assertScreenshot(.serverSelection, step: 2)
|
try await app.assertScreenshot(.serverSelection, step: 2)
|
||||||
XCTAssertFalse(app.buttons[A11yIdentifiers.changeServerScreen.continue].isEnabled, "The confirm button should be disabled when there is an error.")
|
XCTAssertFalse(app.buttons[A11yIdentifiers.changeServerScreen.continue].isEnabled, "The continue button should be disabled when there is an error.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testNonModalPresentation() async throws {
|
func testNonModalPresentation() async throws {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:983a85ed153810102033ab63393860e34bfaafa93008221c7561917eeef2716e
|
oid sha256:f7c8ca16e04390ab80586512f5ec8bd2a93f7cf4f505e1e6eab655d716483413
|
||||||
size 66743
|
size 79966
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f7c8ca16e04390ab80586512f5ec8bd2a93f7cf4f505e1e6eab655d716483413
|
|
||||||
size 79966
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2323bf954c479c1cba9da33d44389875c14538e357e933521b0de7c9c99c6a7f
|
oid sha256:39f4e0c7d6659d6e0e650cc858bba9979b95d053a5d5504201b4f529ce87a3f3
|
||||||
size 68073
|
size 89379
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:39f4e0c7d6659d6e0e650cc858bba9979b95d053a5d5504201b4f529ce87a3f3
|
|
||||||
size 89379
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4256cefea1560f7a604c2e1ff9a58095f4a7c90ea84cc1654a200f1499d4939b
|
oid sha256:7d5c56d3c46647226a54b38ac1b0cfa86bf52deb559cb634a0005c74f6b33424
|
||||||
size 70140
|
size 97924
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:7d5c56d3c46647226a54b38ac1b0cfa86bf52deb559cb634a0005c74f6b33424
|
|
||||||
size 97924
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3bf19067e1d8b000ffd6a5d081054953be94f7f15961d055ee4eac38868f3d0e
|
oid sha256:495068fcb3484f9b1329df608d84c217a019d12b812ebf8676f1987cada089db
|
||||||
size 74768
|
size 122325
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:495068fcb3484f9b1329df608d84c217a019d12b812ebf8676f1987cada089db
|
|
||||||
size 122325
|
|
||||||
Reference in New Issue
Block a user