mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Fix authentication activity indicators when the authentication server is being slow
* Fetch the OIDC login URL as part of the Server Confirmation screen. The loading indicator would show and hide while configuring and then immediately show again while fetching the URL. * Allow the OIDCAuthenticationPresenter to show its own errors. We no longer need to present errors in the authentication flow coordinator.
This commit is contained in:
@@ -144,12 +144,10 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .continue(let window):
|
case .continueWithOIDC(let oidcData, let window):
|
||||||
if authenticationService.homeserver.value.loginMode.supportsOIDCFlow, let window {
|
showOIDCAuthentication(oidcData: oidcData, presentationAnchor: window)
|
||||||
showOIDCAuthentication(presentationAnchor: window)
|
case .continueWithPassword:
|
||||||
} else {
|
showLoginScreen()
|
||||||
showLoginScreen()
|
|
||||||
}
|
|
||||||
case .changeServer:
|
case .changeServer:
|
||||||
showServerSelectionScreen(authenticationFlow: authenticationFlow)
|
showServerSelectionScreen(authenticationFlow: authenticationFlow)
|
||||||
}
|
}
|
||||||
@@ -185,29 +183,21 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
|
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showOIDCAuthentication(presentationAnchor: UIWindow) {
|
private func showOIDCAuthentication(oidcData: OIDCAuthorizationDataProxy, presentationAnchor: UIWindow) {
|
||||||
startLoading()
|
let presenter = OIDCAuthenticationPresenter(authenticationService: authenticationService,
|
||||||
|
oidcRedirectURL: appSettings.oidcRedirectURL,
|
||||||
|
presentationAnchor: presentationAnchor,
|
||||||
|
userIndicatorController: userIndicatorController)
|
||||||
|
oidcPresenter = presenter
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
switch await authenticationService.urlForOIDCLogin() {
|
switch await presenter.authenticate(using: oidcData) {
|
||||||
case .failure(let error):
|
case .success(let userSession):
|
||||||
stopLoading()
|
userHasSignedIn(userSession: userSession)
|
||||||
handleError(error)
|
case .failure:
|
||||||
case .success(let oidcData):
|
break // Nothing to do, the alerts are handled by the presenter.
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
oidcPresenter = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,35 +228,4 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private func userHasSignedIn(userSession: UserSessionProtocol) {
|
private func userHasSignedIn(userSession: UserSessionProtocol) {
|
||||||
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let loadingIndicatorIdentifier = "\(AuthenticationFlowCoordinator.self)-Loading"
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
|
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
|
||||||
state.homeserver.loginMode == .password
|
state.homeserver.loginMode == .password
|
||||||
})
|
})
|
||||||
.previewDisplayName("matrix.org")
|
.previewDisplayName("Initial State")
|
||||||
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
LoginScreen(context: credentialsViewModel.context)
|
LoginScreen(context: credentialsViewModel.context)
|
||||||
@@ -155,7 +155,7 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
.previewDisplayName("Unsupported")
|
.previewDisplayName("Unsupported")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel(homeserverAddress: String = "matrix.org", withCredentials: Bool = false) -> LoginScreenViewModel {
|
static func makeViewModel(homeserverAddress: String = "example.com", withCredentials: Bool = false) -> LoginScreenViewModel {
|
||||||
let authenticationService = AuthenticationService.mock
|
let authenticationService = AuthenticationService.mock
|
||||||
|
|
||||||
Task { await authenticationService.configure(for: homeserverAddress, flow: .login) }
|
Task { await authenticationService.configure(for: homeserverAddress, flow: .login) }
|
||||||
|
|||||||
@@ -13,11 +13,16 @@ class OIDCAuthenticationPresenter: NSObject {
|
|||||||
private let authenticationService: AuthenticationServiceProtocol
|
private let authenticationService: AuthenticationServiceProtocol
|
||||||
private let oidcRedirectURL: URL
|
private let oidcRedirectURL: URL
|
||||||
private let presentationAnchor: UIWindow
|
private let presentationAnchor: UIWindow
|
||||||
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
init(authenticationService: AuthenticationServiceProtocol, oidcRedirectURL: URL, presentationAnchor: UIWindow) {
|
init(authenticationService: AuthenticationServiceProtocol,
|
||||||
|
oidcRedirectURL: URL,
|
||||||
|
presentationAnchor: UIWindow,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
self.oidcRedirectURL = oidcRedirectURL
|
self.oidcRedirectURL = oidcRedirectURL
|
||||||
self.presentationAnchor = presentationAnchor
|
self.presentationAnchor = presentationAnchor
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,21 +44,44 @@ class OIDCAuthenticationPresenter: NSObject {
|
|||||||
if let nsError = error as? NSError,
|
if let nsError = error as? NSError,
|
||||||
nsError.domain == ASWebAuthenticationSessionErrorDomain,
|
nsError.domain == ASWebAuthenticationSessionErrorDomain,
|
||||||
nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
|
nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
|
||||||
|
// No need to show an error here, just abort and return a failure.
|
||||||
await authenticationService.abortOIDCLogin(data: oidcData)
|
await authenticationService.abortOIDCLogin(data: oidcData)
|
||||||
return .failure(.oidcError(.userCancellation))
|
return .failure(.oidcError(.userCancellation))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MXLog.error("Missing callback URL from the web authentication session.")
|
||||||
|
userIndicatorController.alertInfo = AlertInfo(id: UUID())
|
||||||
await authenticationService.abortOIDCLogin(data: oidcData)
|
await authenticationService.abortOIDCLogin(data: oidcData)
|
||||||
return .failure(.oidcError(.unknown))
|
return .failure(.oidcError(.unknown))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exchanging the callback with the homeserver can be slow, so show the loading indicator while we wait (the modal has already been dismissed).
|
||||||
|
startLoading(delay: .milliseconds(50)) // Small delay to handle a cancellation callback without the indicator showing.
|
||||||
|
defer { stopLoading() }
|
||||||
|
|
||||||
switch await authenticationService.loginWithOIDCCallback(url) {
|
switch await authenticationService.loginWithOIDCCallback(url) {
|
||||||
case .success(let userSession):
|
case .success(let userSession):
|
||||||
return .success(userSession)
|
return .success(userSession)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
MXLog.error("Error occurred: \(error)")
|
||||||
|
userIndicatorController.alertInfo = AlertInfo(id: UUID())
|
||||||
return .failure(error)
|
return .failure(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static let loadingIndicatorID = "\(OIDCAuthenticationPresenter.self)-Loading"
|
||||||
|
|
||||||
|
private func startLoading(delay: Duration? = nil) {
|
||||||
|
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorID,
|
||||||
|
type: .modal,
|
||||||
|
title: L10n.commonLoading,
|
||||||
|
persistent: true),
|
||||||
|
delay: delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLoading() {
|
||||||
|
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: ASWebAuthenticationPresentationContextProviding
|
// MARK: ASWebAuthenticationPresentationContextProviding
|
||||||
|
|||||||
+7
-4
@@ -16,7 +16,8 @@ struct ServerConfirmationScreenCoordinatorParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ServerConfirmationScreenCoordinatorAction {
|
enum ServerConfirmationScreenCoordinatorAction {
|
||||||
case `continue`(UIWindow?)
|
case continueWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||||
|
case continueWithPassword
|
||||||
case changeServer
|
case changeServer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,10 +42,12 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .confirm:
|
case .continueWithOIDC(let oidcData, let window):
|
||||||
self.actionsSubject.send(.continue(viewModel.context.viewState.window))
|
actionsSubject.send(.continueWithOIDC(data: oidcData, window: window))
|
||||||
|
case .continueWithPassword:
|
||||||
|
actionsSubject.send(.continueWithPassword)
|
||||||
case .changeServer:
|
case .changeServer:
|
||||||
self.actionsSubject.send(.changeServer)
|
actionsSubject.send(.changeServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
+4
-2
@@ -8,8 +8,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum ServerConfirmationScreenViewModelAction {
|
enum ServerConfirmationScreenViewModelAction {
|
||||||
/// The user would like to continue with the current homeserver.
|
/// Continue the flow using the provided OIDC parameters.
|
||||||
case confirm
|
case continueWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||||
|
/// Continue the flow using password authentication.
|
||||||
|
case continueWithPassword
|
||||||
/// The user would like to change to a different homeserver.
|
/// The user would like to change to a different homeserver.
|
||||||
case changeServer
|
case changeServer
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-8
@@ -48,7 +48,7 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
guard state.window != window else { return }
|
guard state.window != window else { return }
|
||||||
Task { state.window = window }
|
Task { state.window = window }
|
||||||
case .confirm:
|
case .confirm:
|
||||||
Task { await configureAndContinue() }
|
Task { await confirmHomeserver() }
|
||||||
case .changeServer:
|
case .changeServer:
|
||||||
actionsSubject.send(.changeServer)
|
actionsSubject.send(.changeServer)
|
||||||
}
|
}
|
||||||
@@ -56,13 +56,13 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func configureAndContinue() async {
|
private func confirmHomeserver() async {
|
||||||
let homeserver = authenticationService.homeserver.value
|
let homeserver = authenticationService.homeserver.value
|
||||||
|
|
||||||
// If the login mode is unknown, the service hasn't be configured and we need to do it now.
|
// If the login mode is unknown, the service hasn't be configured and we need to do it now.
|
||||||
// Otherwise we can continue the flow as server selection has been performed and succeeded.
|
// Otherwise we can continue the flow as server selection has been performed and succeeded.
|
||||||
guard homeserver.loginMode == .unknown || authenticationService.flow != authenticationFlow else {
|
guard homeserver.loginMode == .unknown || authenticationService.flow != authenticationFlow else {
|
||||||
actionsSubject.send(.confirm)
|
await fetchLoginURLIfNeededAndContinue()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
|
|
||||||
switch await authenticationService.configure(for: homeserver.address, flow: authenticationFlow) {
|
switch await authenticationService.configure(for: homeserver.address, flow: authenticationFlow) {
|
||||||
case .success:
|
case .success:
|
||||||
actionsSubject.send(.confirm)
|
await fetchLoginURLIfNeededAndContinue()
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
switch error {
|
switch error {
|
||||||
case .invalidServer, .invalidHomeserverAddress:
|
case .invalidServer, .invalidHomeserverAddress:
|
||||||
@@ -90,14 +90,39 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startLoading(label: String = L10n.commonLoading) {
|
private func fetchLoginURLIfNeededAndContinue() async {
|
||||||
userIndicatorController.submitIndicator(UserIndicator(type: .modal,
|
guard authenticationService.homeserver.value.loginMode.supportsOIDCFlow else {
|
||||||
title: label,
|
actionsSubject.send(.continueWithPassword)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let window = state.window else {
|
||||||
|
displayError(.unknownError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startLoading() // Uses the same ID, so no need to worry if the indicator already exists
|
||||||
|
defer { stopLoading() }
|
||||||
|
|
||||||
|
switch await authenticationService.urlForOIDCLogin() {
|
||||||
|
case .success(let oidcData):
|
||||||
|
actionsSubject.send(.continueWithOIDC(data: oidcData, window: window))
|
||||||
|
case .failure:
|
||||||
|
displayError(.unknownError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let loadingIndicatorID = "\(ServerConfirmationScreenViewModel.self)-Loading"
|
||||||
|
|
||||||
|
private func startLoading() {
|
||||||
|
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorID,
|
||||||
|
type: .modal,
|
||||||
|
title: L10n.commonLoading,
|
||||||
persistent: true))
|
persistent: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopLoading() {
|
private func stopLoading() {
|
||||||
userIndicatorController.retractAllIndicators()
|
userIndicatorController.retractIndicatorWithId(loadingIndicatorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func displayError(_ type: ServerConfirmationScreenAlert) {
|
private func displayError(_ type: ServerConfirmationScreenAlert) {
|
||||||
|
|||||||
+3
-3
@@ -29,11 +29,11 @@ enum MockSoftLogoutScreenState: String, CaseIterable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .emptyPassword:
|
case .emptyPassword:
|
||||||
return SoftLogoutScreenViewModel(credentials: credentials,
|
return SoftLogoutScreenViewModel(credentials: credentials,
|
||||||
homeserver: .mockMatrixDotOrg,
|
homeserver: .mockBasicServer,
|
||||||
keyBackupNeeded: false)
|
keyBackupNeeded: false)
|
||||||
case .enteredPassword:
|
case .enteredPassword:
|
||||||
return SoftLogoutScreenViewModel(credentials: credentials,
|
return SoftLogoutScreenViewModel(credentials: credentials,
|
||||||
homeserver: .mockMatrixDotOrg,
|
homeserver: .mockBasicServer,
|
||||||
keyBackupNeeded: false,
|
keyBackupNeeded: false,
|
||||||
password: "12345678")
|
password: "12345678")
|
||||||
case .oidc:
|
case .oidc:
|
||||||
@@ -46,7 +46,7 @@ enum MockSoftLogoutScreenState: String, CaseIterable {
|
|||||||
keyBackupNeeded: false)
|
keyBackupNeeded: false)
|
||||||
case .keyBackupNeeded:
|
case .keyBackupNeeded:
|
||||||
return SoftLogoutScreenViewModel(credentials: credentials,
|
return SoftLogoutScreenViewModel(credentials: credentials,
|
||||||
homeserver: .mockMatrixDotOrg,
|
homeserver: .mockBasicServer,
|
||||||
keyBackupNeeded: true)
|
keyBackupNeeded: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -143,7 +143,8 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
let presenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService,
|
let presenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService,
|
||||||
oidcRedirectURL: ServiceLocator.shared.settings.oidcRedirectURL,
|
oidcRedirectURL: ServiceLocator.shared.settings.oidcRedirectURL,
|
||||||
presentationAnchor: presentationAnchor)
|
presentationAnchor: presentationAnchor,
|
||||||
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
self.oidcPresenter = presenter
|
self.oidcPresenter = presenter
|
||||||
switch await presenter.authenticate(using: oidcData) {
|
switch await presenter.authenticate(using: oidcData) {
|
||||||
case .success(let userSession):
|
case .success(let userSession):
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5eb36898636e84943c7b1b2f3ef0289bf133b4da57f4ce3a1ba9c387ce5ea91c
|
oid sha256:2be2a30d2c8204d2ef860e79877bd277b899c0725560f9abf47eb7f5a929f187
|
||||||
size 91146
|
size 92468
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:eb17164648a312eb4a32d91c8908b99d69ec7dc3265b1af7eb91514bbaf74ac6
|
oid sha256:662b49aa4f0466a52723ad538e507604b19b70c551e1b912296cc7d4f82cfeb4
|
||||||
size 95333
|
size 96651
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:324939f6d163efe36c492f67b74b09e56f71948b518e3e0910b11124671b82d7
|
oid sha256:845f7d62abf84d40c8913befa23053423b1fa1c9b2d4a3f173e57e399181c6c3
|
||||||
size 47333
|
size 48439
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0fb309bbd4129b1ad027c8e8ee302a2815adf7c58f9e5de8398b9ce0420851f4
|
oid sha256:b8a14e13a1687c5b1fdeb9a9149105cbf096ff17ff56e19f93bf77d3ba91202b
|
||||||
size 54972
|
size 55382
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:036ac6ac02cff4007cc762a2f0bb1c4f5fa34a15ede708147aecd249a2e69587
|
||||||
|
size 95915
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7fc634e0b161123055a8ddf81269882b37c511b6b8feee0d0b86309350dfae78
|
||||||
|
size 100917
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:47ab21f12dbcdd0ffeb7da8aa0aaf3ed409e7d9f2255979f96a621e41a2206ba
|
||||||
|
size 51503
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:650ebf41da7dc3c084144e00aa4aaf4837ca2ede976cdc5d4f7c6887ea921ac8
|
||||||
|
size 59766
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:8c13f24fb140219627c51b1d680149065901b39d24b773525af663b2c42f536e
|
|
||||||
size 94615
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d277215e66e5b80c96386c819a9a82b5f360cb6639d17b16783cf27ea6390fdf
|
|
||||||
size 99601
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:97ac8c4c12de6ccca253c59eaa9ac0baf6243094d2ed9691ff9ed1cb363fa42a
|
|
||||||
size 50411
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c0216e37d95512a2f03bb4fdc2cf903ecc05d62400e642ed9e6cadb8590af24e
|
|
||||||
size 59387
|
|
||||||
@@ -16,6 +16,12 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
|
|||||||
// Splash Screen: Tap get started button
|
// Splash Screen: Tap get started button
|
||||||
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
||||||
|
|
||||||
|
// Server Confirmation: Tap change server button
|
||||||
|
app.buttons[A11yIdentifiers.serverConfirmationScreen.changeServer].tap()
|
||||||
|
|
||||||
|
// Server Selection: Clear the default, enter OIDC server and continue.
|
||||||
|
app.textFields[A11yIdentifiers.changeServerScreen.server].clearAndTypeText("example.com\n", app: app)
|
||||||
|
|
||||||
// Server Confirmation: Tap continue button
|
// Server Confirmation: Tap continue button
|
||||||
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
||||||
|
|
||||||
@@ -40,6 +46,12 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
|
|||||||
// Splash Screen: Tap get started button
|
// Splash Screen: Tap get started button
|
||||||
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
||||||
|
|
||||||
|
// Server Confirmation: Tap change server button
|
||||||
|
app.buttons[A11yIdentifiers.serverConfirmationScreen.changeServer].tap()
|
||||||
|
|
||||||
|
// Server Selection: Clear the default, enter OIDC server and continue.
|
||||||
|
app.textFields[A11yIdentifiers.changeServerScreen.server].clearAndTypeText("example.com\n", app: app)
|
||||||
|
|
||||||
// Server Confirmation: Tap continue button
|
// Server Confirmation: Tap continue button
|
||||||
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
||||||
|
|
||||||
@@ -66,6 +78,12 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
|
|||||||
// Splash Screen: Tap get started button
|
// Splash Screen: Tap get started button
|
||||||
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
||||||
|
|
||||||
|
// Server Confirmation: Tap change server button
|
||||||
|
app.buttons[A11yIdentifiers.serverConfirmationScreen.changeServer].tap()
|
||||||
|
|
||||||
|
// Server Selection: Clear the default, enter OIDC server and continue.
|
||||||
|
app.textFields[A11yIdentifiers.changeServerScreen.server].clearAndTypeText("example.com\n", app: app)
|
||||||
|
|
||||||
// Server Confirmation: Tap continue button
|
// Server Confirmation: Tap continue button
|
||||||
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
||||||
|
|
||||||
@@ -96,7 +114,8 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
|
|||||||
// Server Confirmation: Tap continue button
|
// Server Confirmation: Tap continue button
|
||||||
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
|
||||||
|
|
||||||
// Then the login form shouldn't be shown as OIDC will be used instead.
|
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||||
XCTAssertFalse(app.buttons[A11yIdentifiers.loginScreen.continue].waitForExistence(timeout: 1), "The login screen should not be shown after selecting a homeserver with OIDC.")
|
XCTAssertTrue(springboard.staticTexts["“ElementX” Wants to Use “company.com” to Sign In"].waitForExistence(timeout: 2),
|
||||||
|
"The web authentication prompt should be shown after selecting a homeserver with OIDC.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import XCTest
|
|||||||
@MainActor
|
@MainActor
|
||||||
class ServerConfirmationScreenViewModelTests: XCTestCase {
|
class ServerConfirmationScreenViewModelTests: XCTestCase {
|
||||||
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
||||||
|
var client: ClientSDKMock!
|
||||||
var service: AuthenticationServiceProtocol!
|
var service: AuthenticationServiceProtocol!
|
||||||
|
|
||||||
var viewModel: ServerConfirmationScreenViewModel!
|
var viewModel: ServerConfirmationScreenViewModel!
|
||||||
@@ -22,15 +23,18 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
setupViewModel(authenticationFlow: .login)
|
setupViewModel(authenticationFlow: .login)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
context.send(viewAction: .confirm)
|
context.send(viewAction: .confirm)
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
// Then a call to configure service should be made.
|
// Then a call to configure service should be made.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .consent)
|
||||||
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testConfirmLoginAfterConfiguration() async throws {
|
func testConfirmLoginAfterConfiguration() async throws {
|
||||||
@@ -40,16 +44,19 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
XCTFail("The configuration should succeed.")
|
XCTFail("The configuration should succeed.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
context.send(viewAction: .confirm)
|
context.send(viewAction: .confirm)
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
// Then the configured homeserver should be used and no additional call should be made to the service.
|
// Then the configured homeserver should be used and no additional client should be built.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .consent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testConfirmRegisterWithoutConfiguration() async throws {
|
func testConfirmRegisterWithoutConfiguration() async throws {
|
||||||
@@ -57,15 +64,19 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
setupViewModel(authenticationFlow: .register)
|
setupViewModel(authenticationFlow: .register)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
context.send(viewAction: .confirm)
|
context.send(viewAction: .confirm)
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
// Then a call to configure service should be made.
|
// Then a call to configure service should be made.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||||
|
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||||
|
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||||
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testConfirmRegisterAfterConfiguration() async throws {
|
func testConfirmRegisterAfterConfiguration() async throws {
|
||||||
@@ -75,16 +86,59 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
XCTFail("The configuration should succeed.")
|
XCTFail("The configuration should succeed.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
context.send(viewAction: .confirm)
|
context.send(viewAction: .confirm)
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
// Then the configured homeserver should be used and no additional call should be made to the service.
|
// Then the configured homeserver should be used and no additional client should be built.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||||
|
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfirmPasswordLoginWithoutConfiguration() async throws {
|
||||||
|
// Given a view model for login using a service that hasn't been configured (against a server that doesn't support OIDC).
|
||||||
|
setupViewModel(authenticationFlow: .login, supportsOIDC: false)
|
||||||
|
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||||
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
|
|
||||||
|
// When continuing from the confirmation screen.
|
||||||
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||||
|
context.send(viewAction: .confirm)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
// Then a call to configure service should be made, but not for the OIDC URL.
|
||||||
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
|
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfirmPasswordLoginAfterConfiguration() async throws {
|
||||||
|
// Given a view model for login using a service that has already been configured (via the server selection screen).
|
||||||
|
setupViewModel(authenticationFlow: .login, supportsOIDC: false)
|
||||||
|
guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .login) else {
|
||||||
|
XCTFail("The configuration should succeed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||||
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
|
|
||||||
|
// When continuing from the confirmation screen.
|
||||||
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||||
|
context.send(viewAction: .confirm)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
// Then the configured homeserver should be used and no additional client should be built, nor a call to get the OIDC URL.
|
||||||
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRegistrationNotSupportedAlert() async throws {
|
func testRegistrationNotSupportedAlert() async throws {
|
||||||
@@ -126,9 +180,9 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
private func setupViewModel(authenticationFlow: AuthenticationFlow, supportsOIDC: Bool = true, supportsOIDCCreatePrompt: Bool = true, supportsPasswordLogin: Bool = true) {
|
private func setupViewModel(authenticationFlow: AuthenticationFlow, supportsOIDC: Bool = true, supportsOIDCCreatePrompt: Bool = true, supportsPasswordLogin: Bool = true) {
|
||||||
// Manually create a configuration as the default homeserver address setting is immutable.
|
// Manually create a configuration as the default homeserver address setting is immutable.
|
||||||
let client = ClientSDKMock(configuration: .init(oidcLoginURL: supportsOIDC ? "https://account.matrix.org/authorize" : nil,
|
client = ClientSDKMock(configuration: .init(oidcLoginURL: supportsOIDC ? "https://account.matrix.org/authorize" : nil,
|
||||||
supportsOIDCCreatePrompt: supportsOIDCCreatePrompt,
|
supportsOIDCCreatePrompt: supportsOIDCCreatePrompt,
|
||||||
supportsPasswordLogin: supportsPasswordLogin))
|
supportsPasswordLogin: supportsPasswordLogin))
|
||||||
let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client],
|
let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client],
|
||||||
qrCodeClient: client)
|
qrCodeClient: client)
|
||||||
|
|
||||||
@@ -143,5 +197,24 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
authenticationFlow: authenticationFlow,
|
authenticationFlow: authenticationFlow,
|
||||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
// Add a fake window in order for the OIDC flow to continue
|
||||||
|
viewModel.context.send(viewAction: .updateWindow(UIWindow()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ServerConfirmationScreenViewModelAction {
|
||||||
|
var isContinueWithOIDC: Bool {
|
||||||
|
switch self {
|
||||||
|
case .continueWithOIDC: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isContinueWithPassword: Bool {
|
||||||
|
switch self {
|
||||||
|
case .continueWithPassword: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user