Allow registration on matrix.org using a custom helper URL. (#3282)

This commit is contained in:
Doug 2024-09-16 13:05:22 +01:00 committed by GitHub
parent 6cfe09b96d
commit 6efbf6117f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 129 additions and 42 deletions

View File

@ -159,6 +159,9 @@ final class AppSettings {
staticRegistrations: oidcStaticRegistrations.mapKeys { $0.absoluteString },
dynamicRegistrationsFile: .sessionsBaseDirectory.appending(path: "oidc/registrations.json"))
/// A temporary hack to allow registration on matrix.org until MAS is deployed.
let webRegistrationEnabled = true
// MARK: - Notifications
var pusherAppId: String {

View File

@ -77,7 +77,8 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - Private
private func showStartScreen() {
let coordinator = AuthenticationStartScreenCoordinator()
let parameters = AuthenticationStartScreenParameters(webRegistrationEnabled: appSettings.webRegistrationEnabled)
let coordinator = AuthenticationStartScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in
@ -85,9 +86,11 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .loginManually:
Task { await self.startAuthentication() }
Task { await self.startAuthentication(flow: .login) }
case .loginWithQR:
startQRCodeLogin()
case .register:
Task { await self.startAuthentication(flow: .register) }
case .reportProblem:
showReportProblemScreen()
}
@ -110,7 +113,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .signInManually:
navigationStackCoordinator.setSheetCoordinator(nil)
Task { await self.startAuthentication() }
Task { await self.startAuthentication(flow: .login) }
case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil)
case .done(let userSession):
@ -134,20 +137,20 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
bugReportFlowCoordinator?.start()
}
private func startAuthentication() async {
private func startAuthentication(flow: AuthenticationFlow) async {
startLoading()
switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) {
case .success:
stopLoading()
showServerConfirmationScreen()
showServerConfirmationScreen(authenticationFlow: flow)
case .failure:
stopLoading()
showServerSelectionScreen(isModallyPresented: false)
showServerSelectionScreen(authenticationFlow: flow, isModallyPresented: false)
}
}
private func showServerSelectionScreen(isModallyPresented: Bool) {
private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow, isModallyPresented: Bool) {
let navigationCoordinator = NavigationStackCoordinator()
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
@ -166,13 +169,18 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
} 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()
if authenticationFlow == .login {
// Add the password login screen directly to the flow, its fine.
showLoginScreen()
} else {
// Add the web registration screen directly to the flow, its fine.
showWebRegistration()
}
} else {
// OIDC is presented from the confirmation screen so replace the
// server selection screen which was inserted to handle the failure.
navigationStackCoordinator.pop()
showServerConfirmationScreen()
showServerConfirmationScreen(authenticationFlow: authenticationFlow)
}
}
case .dismiss:
@ -189,9 +197,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func showServerConfirmationScreen() {
private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) {
let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService,
authenticationFlow: .login)
authenticationFlow: authenticationFlow)
let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
@ -201,11 +209,13 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
case .continue(let window):
if authenticationService.homeserver.value.loginMode == .oidc, let window {
showOIDCAuthentication(presentationAnchor: window)
} else if authenticationFlow == .register {
showWebRegistration()
} else {
showLoginScreen()
}
case .changeServer:
showServerSelectionScreen(isModallyPresented: true)
showServerSelectionScreen(authenticationFlow: authenticationFlow, isModallyPresented: true)
}
}
.store(in: &cancellables)
@ -213,6 +223,26 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.push(coordinator)
}
private func showWebRegistration() {
let parameters = WebRegistrationScreenCoordinatorParameters(authenticationService: authenticationService,
userIndicatorController: userIndicatorController)
let coordinator = WebRegistrationScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil)
case .signedIn(let userSession):
userHasSignedIn(userSession: userSession)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setSheetCoordinator(coordinator)
}
private func showOIDCAuthentication(presentationAnchor: UIWindow) {
startLoading()

View File

@ -17,11 +17,12 @@ struct LoginHomeserver: Equatable {
var registrationHelperURL: URL?
/// Creates a new homeserver value.
init(address: String, loginMode: LoginMode) {
init(address: String, loginMode: LoginMode, registrationHelperURL: URL? = nil) {
let address = Self.sanitized(address).components(separatedBy: "://").last ?? address
self.address = address
self.loginMode = loginMode
self.registrationHelperURL = registrationHelperURL
}
/// Sanitizes a user entered homeserver address with the following rules
@ -47,7 +48,7 @@ struct LoginHomeserver: Equatable {
extension LoginHomeserver {
/// A mock homeserver that is configured just like matrix.org.
static var mockMatrixDotOrg: LoginHomeserver {
LoginHomeserver(address: "matrix.org", loginMode: .password)
LoginHomeserver(address: "matrix.org", loginMode: .password, registrationHelperURL: "https://develop.element.io/#/mobile_register")
}
/// A mock homeserver that supports login and registration via a password but has no SSO providers.

View File

@ -19,6 +19,8 @@ struct ServerConfirmationScreenViewState: BindableState {
var homeserverAddress: String
/// The flow being attempted on the selected homeserver.
let authenticationFlow: AuthenticationFlow
/// Whether or not the homeserver supports registration.
var homeserverSupportsRegistration = false
/// The presentation anchor used for OIDC authentication.
var window: UIWindow?
@ -37,14 +39,26 @@ struct ServerConfirmationScreenViewState: BindableState {
switch authenticationFlow {
case .login:
if homeserverAddress == "matrix.org" {
return L10n.screenServerConfirmationMessageLoginMatrixDotOrg
L10n.screenServerConfirmationMessageLoginMatrixDotOrg
} else if homeserverAddress == "element.io" {
return L10n.screenServerConfirmationMessageLoginElementDotIo
L10n.screenServerConfirmationMessageLoginElementDotIo
} else {
return ""
""
}
case .register:
return L10n.screenServerConfirmationMessageRegister
if canContinue {
L10n.screenServerConfirmationMessageRegister
} else {
L10n.errorAccountCreationNotPossible
}
}
}
/// Whether or not it is valid to continue the flow.
var canContinue: Bool {
switch authenticationFlow {
case .login: true
case .register: homeserverSupportsRegistration
}
}
}

View File

@ -18,14 +18,18 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
}
init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow) {
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: authenticationService.homeserver.value.address,
authenticationFlow: authenticationFlow))
let homeserver = authenticationService.homeserver.value
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address,
authenticationFlow: authenticationFlow,
homeserverSupportsRegistration: homeserver.supportsRegistration))
authenticationService.homeserver
.receive(on: DispatchQueue.main)
.sink { [weak self] homeserver in
guard let self else { return }
state.homeserverAddress = homeserver.address
state.homeserverSupportsRegistration = homeserver.supportsRegistration
}
.store(in: &cancellables)
}
@ -44,3 +48,9 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
}
}
}
extension LoginHomeserver {
var supportsRegistration: Bool {
loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil)
}
}

View File

@ -5,6 +5,7 @@
// Please see LICENSE in the repository root for full details.
//
import Compound
import SwiftUI
struct ServerConfirmationScreen: View {
@ -52,6 +53,7 @@ struct ServerConfirmationScreen: View {
}
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue)
.disabled(!context.viewState.canContinue)
Button { context.send(viewAction: .changeServer) } label: {
Text(L10n.screenServerConfirmationChangeServer)

View File

@ -8,6 +8,10 @@
import Combine
import SwiftUI
struct AuthenticationStartScreenParameters {
let webRegistrationEnabled: Bool
}
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
private var viewModel: AuthenticationStartScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AuthenticationStartScreenCoordinatorAction, Never> = .init()
@ -17,8 +21,8 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
actionsSubject.eraseToAnyPublisher()
}
init() {
viewModel = AuthenticationStartScreenViewModel()
init(parameters: AuthenticationStartScreenParameters) {
viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: parameters.webRegistrationEnabled)
}
// MARK: - Public
@ -33,6 +37,8 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.loginManually)
case .loginWithQR:
actionsSubject.send(.loginWithQR)
case .register:
actionsSubject.send(.register)
case .reportProblem:
actionsSubject.send(.reportProblem)
}

View File

@ -12,21 +12,25 @@ import SwiftUI
enum AuthenticationStartScreenCoordinatorAction {
case loginManually
case loginWithQR
case register
case reportProblem
}
enum AuthenticationStartScreenViewModelAction {
case loginManually
case loginWithQR
case register
case reportProblem
}
struct AuthenticationStartScreenViewState: BindableState {
var isQRCodeLoginEnabled = false
let isWebRegistrationEnabled: Bool
let isQRCodeLoginEnabled: Bool
}
enum AuthenticationStartScreenViewAction {
case loginManually
case loginWithQR
case register
case reportProblem
}

View File

@ -17,9 +17,9 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: AuthenticationStartScreenViewState())
state.isQRCodeLoginEnabled = !ProcessInfo.processInfo.isiOSAppOnMac && AppSettings.isDevelopmentBuild
init(webRegistrationEnabled: Bool) {
super.init(initialViewState: AuthenticationStartScreenViewState(isWebRegistrationEnabled: webRegistrationEnabled,
isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac && AppSettings.isDevelopmentBuild))
}
override func process(viewAction: AuthenticationStartScreenViewAction) {
@ -28,6 +28,8 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
actionsSubject.send(.loginManually)
case .loginWithQR:
actionsSubject.send(.loginWithQR)
case .register:
actionsSubject.send(.register)
case .reportProblem:
actionsSubject.send(.reportProblem)
}

View File

@ -99,6 +99,14 @@ struct AuthenticationStartScreen: View {
}
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signIn)
if context.viewState.isWebRegistrationEnabled {
Button { context.send(viewAction: .register) } label: {
Text(L10n.screenCreateAccountTitle)
.padding(14)
}
.buttonStyle(.compound(.plain))
}
}
.padding(.horizontal, verticalSizeClass == .compact ? 128 : 24)
.readableFrame()
@ -108,7 +116,7 @@ struct AuthenticationStartScreen: View {
// MARK: - Previews
struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = AuthenticationStartScreenViewModel()
static let viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: true)
static var previews: some View {
AuthenticationStartScreen(context: viewModel.context)

View File

@ -25,12 +25,19 @@ class ServerConfirmationScreenViewStateTests: XCTestCase {
}
func testRegisterMessageString() {
let matrixDotOrgLogin = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address,
authenticationFlow: .register)
XCTAssertEqual(matrixDotOrgLogin.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
let matrixDotOrgRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address,
authenticationFlow: .register,
homeserverSupportsRegistration: true)
XCTAssertEqual(matrixDotOrgRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
let otherLogin = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address,
authenticationFlow: .register)
XCTAssertEqual(otherLogin.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
let oidcRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address,
authenticationFlow: .register,
homeserverSupportsRegistration: true)
XCTAssertEqual(oidcRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
let otherRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockBasicServer.address,
authenticationFlow: .register,
homeserverSupportsRegistration: false)
XCTAssertEqual(otherRegister.message, L10n.errorAccountCreationNotPossible, "The registration message should always be the same.")
}
}