Skip to content

Commit

Permalink
Merge pull request #6210 from vector-im/ismail/6176_auth_fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
ismailgulek authored May 31, 2022
2 parents 8e99d54 + daf7035 commit a9886e4
Show file tree
Hide file tree
Showing 21 changed files with 259 additions and 20 deletions.
92 changes: 79 additions & 13 deletions Riot/Modules/Onboarding/AuthenticationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,23 +110,33 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc

/// Starts the authentication flow.
@MainActor private func startAuthenticationFlow() async {
do {
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
try await authenticationService.startFlow(flow, for: homeserverAddress)
} catch {
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
displayError(message: error.localizedDescription)
return
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
if initialScreen != .selectServerForRegistration {
do {
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
try await authenticationService.startFlow(flow, for: homeserverAddress)
} catch {
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
displayError(message: error.localizedDescription)
return
}
}

switch initialScreen {
case .registration:
showRegistrationScreen()
if authenticationService.state.homeserver.needsRegistrationFallback {
showFallback(for: flow)
} else {
showRegistrationScreen()
}
case .selectServerForRegistration:
showServerSelectionScreen()
case .login:
showLoginScreen()
if authenticationService.state.homeserver.needsLoginFallback {
showFallback(for: flow)
} else {
showLoginScreen()
}
}
}

Expand Down Expand Up @@ -195,6 +205,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
password = loginPassword
authenticationType = .password
onSessionCreated(session: session, flow: .login)
case .fallback:
showFallback(for: .login)
}
}

Expand Down Expand Up @@ -231,7 +243,11 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
didCompleteWith result: AuthenticationServerSelectionCoordinatorResult) {
switch result {
case .updated:
showRegistrationScreen()
if authenticationService.state.homeserver.needsRegistrationFallback {
showFallback(for: .register)
} else {
showRegistrationScreen()
}
case .dismiss:
MXLog.failure("[AuthenticationCoordinator] AuthenticationServerSelectionScreen is requesting dismiss when part of a stack.")
}
Expand Down Expand Up @@ -273,6 +289,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
password = registerPassword
authenticationType = .password
handleRegistrationResult(result)
case .fallback:
showFallback(for: .register)
}
}

Expand Down Expand Up @@ -407,8 +425,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
case .dummy:
MXLog.failure("[AuthenticationCoordinator] Attempting to perform the dummy stage.")
case .other:
#warning("Show fallback")
MXLog.failure("[AuthenticationCoordinator] Attempting to perform an unsupported stage.")
showFallback(for: .register)
}
}

Expand Down Expand Up @@ -446,6 +464,34 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
}

// MARK: - Additional Screens

private func showFallback(for flow: AuthenticationFlow) {
let url = authenticationService.fallbackURL(for: flow)

MXLog.debug("[AuthenticationCoordinator] showFallback for: \(flow), url: \(url)")

guard let fallbackVC = AuthFallBackViewController(url: url.absoluteString) else {
MXLog.error("[AuthenticationCoordinator] showFallback: could not create fallback view controller")
return
}
fallbackVC.delegate = self
let navController = RiotNavigationController(rootViewController: fallbackVC)
navController.navigationBar.topItem?.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(dismissFallback))
navigationRouter.present(navController, animated: true)
}

@objc
private func dismissFallback() {
MXLog.debug("[AuthenticationCoorrdinator] dismissFallback")

guard let fallbackNavigationVC = navigationRouter.toPresentable().presentedViewController as? RiotNavigationController else {
return
}
fallbackNavigationVC.dismiss(animated: true)
authenticationService.reset()
}

/// Replace the contents of the navigation router with a loading animation.
private func showLoadingAnimation() {
Expand Down Expand Up @@ -623,3 +669,23 @@ extension AuthenticationCoordinator {
// unused
}
}

// MARK: - AuthFallBackViewControllerDelegate
extension AuthenticationCoordinator: AuthFallBackViewControllerDelegate {
func authFallBackViewController(_ authFallBackViewController: AuthFallBackViewController,
didLoginWith loginResponse: MXLoginResponse) {
let credentials = MXCredentials(loginResponse: loginResponse, andDefaultCredentials: nil)
let client = MXRestClient(credentials: credentials)
guard let session = MXSession(matrixRestClient: client) else {
MXLog.failure("[AuthenticationCoordinator] authFallBackViewController:didLogin: session could not be created")
return
}
authenticationType = .other
Task { await onSessionCreated(session: session, flow: authenticationService.state.flow) }
}

func authFallBackViewControllerDidClose(_ authFallBackViewController: AuthFallBackViewController) {
dismissFallback()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,14 @@ extension AuthenticationHomeserverViewData {
showRegistrationForm: false,
ssoIdentityProviders: [SSOIdentityProvider(id: "test", name: "SAML", brand: nil, iconURL: nil)])
}

/// A mock homeserver that supports only supports authentication via fallback.
static var mockFallback: AuthenticationHomeserverViewData {
AuthenticationHomeserverViewData(address: "company.com",
isMatrixDotOrg: false,
showLoginForm: false,
showRegistrationForm: false,
ssoIdentityProviders: [])
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class AuthenticationService: NSObject {
func reset() {
loginWizard = nil
registrationWizard = nil

// The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway.
let address = state.homeserver.addressFromUser ?? state.homeserver.address
self.state = AuthenticationState(flow: .login, homeserverAddress: address)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,26 @@ struct AuthenticationState {
AuthenticationHomeserverViewData(address: displayableAddress,
isMatrixDotOrg: isMatrixDotOrg,
showLoginForm: preferredLoginMode.supportsPasswordFlow,
showRegistrationForm: registrationFlow != nil,
showRegistrationForm: registrationFlow != nil && !needsRegistrationFallback,
ssoIdentityProviders: preferredLoginMode.ssoIdentityProviders ?? [])
}

/// Needs authentication fallback for login
var needsLoginFallback: Bool {
return preferredLoginMode.isUnsupported
}

/// Needs authentication fallback for registration
var needsRegistrationFallback: Bool {
guard let flow = registrationFlow else {
return false
}
switch flow {
case .flowResponse(let result):
return result.needsFallback
default:
return false
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ enum LoginMode {
return false
}
}

var isUnsupported: Bool {
switch self {
case .unsupported:
return true
default:
return false
}
}
}

/// Data obtained when calling `LoginWizard.resetPassword` that will be used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ enum AuthenticationLoginViewModelResult {
case login(username: String, password: String)
/// Continue using the supplied SSO provider.
case continueWithSSO(SSOIdentityProvider)
/// Continue using the fallback page
case fallback
}

// MARK: View
Expand Down Expand Up @@ -70,6 +72,8 @@ enum AuthenticationLoginViewAction {
case forgotPassword
/// Continue using the input username and password.
case next
/// Continue using the fallback page
case fallback
/// Continue using the supplied SSO provider.
case continueWithSSO(SSOIdentityProvider)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class AuthenticationLoginViewModel: AuthenticationLoginViewModelType, Authentica
Task { await callback?(.forgotPassword) }
case .next:
Task { await callback?(.login(username: state.bindings.username, password: state.bindings.password)) }
case .fallback:
Task { await callback?(.fallback) }
case .continueWithSSO(let provider):
Task { await callback?(.continueWithSSO(provider))}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ enum AuthenticationLoginCoordinatorResult {
case continueWithSSO(SSOIdentityProvider)
/// Login was successful with the associated session created.
case success(session: MXSession, password: String)
/// Login requested a fallback
case fallback
}

final class AuthenticationLoginCoordinator: Coordinator, Presentable {
Expand Down Expand Up @@ -109,6 +111,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
self.login(username: username, password: password)
case .continueWithSSO(let identityProvider):
self.callback?(.continueWithSSO(identityProvider))
case .fallback:
self.callback?(.fallback)
}
}
}
Expand Down Expand Up @@ -222,11 +226,11 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
/// Handles the result from the server selection modal, dismissing it after updating the view.
@MainActor private func serverSelectionCoordinator(_ coordinator: AuthenticationServerSelectionCoordinator,
didCompleteWith result: AuthenticationServerSelectionCoordinatorResult) {
if result == .updated {
updateViewModel()
}

navigationRouter.dismissModule(animated: true) { [weak self] in
if result == .updated {
self?.updateViewModel()
}

self?.remove(childCoordinator: coordinator)
}
}
Expand All @@ -235,5 +239,9 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
@MainActor private func updateViewModel() {
let homeserver = authenticationService.state.homeserver
authenticationLoginViewModel.update(homeserver: homeserver.viewData)

if homeserver.needsLoginFallback {
callback?(.fallback)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ enum MockAuthenticationLoginScreenState: MockScreenState, CaseIterable {
case passwordOnly
case passwordWithCredentials
case ssoOnly
case fallback

/// The associated screen
var screenType: Any.Type {
Expand All @@ -47,6 +48,8 @@ enum MockAuthenticationLoginScreenState: MockScreenState, CaseIterable {
viewModel.context.password = "password"
case .ssoOnly:
viewModel = AuthenticationLoginViewModel(homeserver: .mockEnterpriseSSO)
case .fallback:
viewModel = AuthenticationLoginViewModel(homeserver: .mockFallback)
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class AuthenticationLoginUITests: MockScreenTest {
validateServerDescriptionIsHidden(for: state)
validateLoginFormIsHidden(for: state)
validateSSOButtonsAreShown(for: state)
case .fallback:
let state = "a fallback server"
validateFallback(for: state)
}
}

Expand Down Expand Up @@ -114,5 +117,20 @@ class AuthenticationLoginUITests: MockScreenTest {
XCTAssertTrue(nextButton.exists, "The next button should be shown.")
XCTAssertTrue(nextButton.isEnabled, "The next button should be enabled for \(state).")
}

func validateFallback(for state: String) {
let usernameTextField = app.textFields.element
let passwordTextField = app.secureTextFields.element
let nextButton = app.buttons["nextButton"]
let ssoButtons = app.buttons.matching(identifier: "ssoButton")
let fallbackButton = app.buttons["fallbackButton"]

XCTAssertFalse(usernameTextField.exists, "Username input should not be shown for \(state).")
XCTAssertFalse(passwordTextField.exists, "Password input should not be shown for \(state).")
XCTAssertFalse(nextButton.exists, "The next button should not be shown for \(state).")
XCTAssertEqual(ssoButtons.count, 0, "There should not be any SSO buttons shown for \(state).")
XCTAssertTrue(fallbackButton.exists, "The fallback button should be shown for \(state).")
XCTAssertTrue(fallbackButton.isEnabled, "The fallback button should be enabled for \(state).")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,16 @@ class AuthenticationLoginViewModelTests: XCTestCase {
// Then the view state should reflect that the homeserver is now loaded.
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
}

@MainActor func testFallbackServer() {
// Given a basic server example.com that only supports password registration.
let homeserver = AuthenticationHomeserverViewData.mockFallback

// When updating the view model with the server.
viewModel.update(homeserver: homeserver)

// Then the view state should be updated with the homeserver and hide the SSO buttons and login form.
XCTAssertFalse(context.viewState.showSSOButtons, "The SSO buttons should not be shown.")
XCTAssertFalse(context.viewState.homeserver.showLoginForm, "The login form should not be shown.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ struct AuthenticationLoginScreen: View {
ssoButtons
.padding(.top, 16)
}

if !viewModel.viewState.homeserver.showLoginForm && !viewModel.viewState.showSSOButtons {
fallbackButton
}

}
.readableFrame()
Expand Down Expand Up @@ -139,6 +143,15 @@ struct AuthenticationLoginScreen: View {
}
}
}

/// A fallback button that can be used for login.
var fallbackButton: some View {
Button(action: fallback) {
Text(VectorL10n.login)
}
.buttonStyle(PrimaryActionButtonStyle())
.accessibilityIdentifier("fallbackButton")
}

/// Parses the username for a homeserver.
func usernameEditingChanged(isEditing: Bool) {
Expand All @@ -158,6 +171,11 @@ struct AuthenticationLoginScreen: View {
guard viewModel.viewState.hasValidCredentials else { return }
viewModel.send(viewAction: .next)
}

/// Sends the `fallback` view action.
func fallback() {
viewModel.send(viewAction: .fallback)
}
}

// MARK: - Previews
Expand Down
Loading

0 comments on commit a9886e4

Please sign in to comment.