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
15 changed files with 129 additions and 42 deletions
@@ -159,6 +159,9 @@ final class AppSettings {
staticRegistrations: oidcStaticRegistrations.mapKeys { $0.absoluteString }, staticRegistrations: oidcStaticRegistrations.mapKeys { $0.absoluteString },
dynamicRegistrationsFile: .sessionsBaseDirectory.appending(path: "oidc/registrations.json")) 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 // MARK: - Notifications
var pusherAppId: String { var pusherAppId: String {
@@ -77,7 +77,8 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - Private // MARK: - Private
private func showStartScreen() { private func showStartScreen() {
let coordinator = AuthenticationStartScreenCoordinator() let parameters = AuthenticationStartScreenParameters(webRegistrationEnabled: appSettings.webRegistrationEnabled)
let coordinator = AuthenticationStartScreenCoordinator(parameters: parameters)
coordinator.actions coordinator.actions
.sink { [weak self] action in .sink { [weak self] action in
@@ -85,9 +86,11 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .loginManually: case .loginManually:
Task { await self.startAuthentication() } Task { await self.startAuthentication(flow: .login) }
case .loginWithQR: case .loginWithQR:
startQRCodeLogin() startQRCodeLogin()
case .register:
Task { await self.startAuthentication(flow: .register) }
case .reportProblem: case .reportProblem:
showReportProblemScreen() showReportProblemScreen()
} }
@@ -110,7 +113,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .signInManually: case .signInManually:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
Task { await self.startAuthentication() } Task { await self.startAuthentication(flow: .login) }
case .cancel: case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
case .done(let userSession): case .done(let userSession):
@@ -134,20 +137,20 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
bugReportFlowCoordinator?.start() bugReportFlowCoordinator?.start()
} }
private func startAuthentication() async { private func startAuthentication(flow: AuthenticationFlow) async {
startLoading() startLoading()
switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) { switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) {
case .success: case .success:
stopLoading() stopLoading()
showServerConfirmationScreen() showServerConfirmationScreen(authenticationFlow: flow)
case .failure: case .failure:
stopLoading() 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 navigationCoordinator = NavigationStackCoordinator()
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService, let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
@@ -166,13 +169,18 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
} else { } else {
// We are here because the default server failed to respond. // We are here because the default server failed to respond.
if authenticationService.homeserver.value.loginMode == .password { if authenticationService.homeserver.value.loginMode == .password {
// Add the password login screen directly to the flow, its fine. if authenticationFlow == .login {
showLoginScreen() // 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 { } else {
// OIDC is presented from the confirmation screen so replace the // OIDC is presented from the confirmation screen so replace the
// server selection screen which was inserted to handle the failure. // server selection screen which was inserted to handle the failure.
navigationStackCoordinator.pop() navigationStackCoordinator.pop()
showServerConfirmationScreen() showServerConfirmationScreen(authenticationFlow: authenticationFlow)
} }
} }
case .dismiss: case .dismiss:
@@ -189,9 +197,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func showServerConfirmationScreen() { private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) {
let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService, let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService,
authenticationFlow: .login) authenticationFlow: authenticationFlow)
let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters) let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in coordinator.actions.sink { [weak self] action in
@@ -201,11 +209,13 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
case .continue(let window): case .continue(let window):
if authenticationService.homeserver.value.loginMode == .oidc, let window { if authenticationService.homeserver.value.loginMode == .oidc, let window {
showOIDCAuthentication(presentationAnchor: window) showOIDCAuthentication(presentationAnchor: window)
} else if authenticationFlow == .register {
showWebRegistration()
} else { } else {
showLoginScreen() showLoginScreen()
} }
case .changeServer: case .changeServer:
showServerSelectionScreen(isModallyPresented: true) showServerSelectionScreen(authenticationFlow: authenticationFlow, isModallyPresented: true)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -213,6 +223,26 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.push(coordinator) 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) { private func showOIDCAuthentication(presentationAnchor: UIWindow) {
startLoading() startLoading()
@@ -17,11 +17,12 @@ struct LoginHomeserver: Equatable {
var registrationHelperURL: URL? var registrationHelperURL: URL?
/// Creates a new homeserver value. /// 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 let address = Self.sanitized(address).components(separatedBy: "://").last ?? address
self.address = address self.address = address
self.loginMode = loginMode self.loginMode = loginMode
self.registrationHelperURL = registrationHelperURL
} }
/// Sanitizes a user entered homeserver address with the following rules /// Sanitizes a user entered homeserver address with the following rules
@@ -47,7 +48,7 @@ struct LoginHomeserver: Equatable {
extension LoginHomeserver { extension LoginHomeserver {
/// A mock homeserver that is configured just like matrix.org. /// A mock homeserver that is configured just like matrix.org.
static var mockMatrixDotOrg: LoginHomeserver { 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. /// A mock homeserver that supports login and registration via a password but has no SSO providers.
@@ -19,6 +19,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
/// Whether or not the homeserver supports registration.
var homeserverSupportsRegistration = false
/// The presentation anchor used for OIDC authentication. /// The presentation anchor used for OIDC authentication.
var window: UIWindow? var window: UIWindow?
@@ -37,14 +39,26 @@ struct ServerConfirmationScreenViewState: BindableState {
switch authenticationFlow { switch authenticationFlow {
case .login: case .login:
if homeserverAddress == "matrix.org" { if homeserverAddress == "matrix.org" {
return L10n.screenServerConfirmationMessageLoginMatrixDotOrg L10n.screenServerConfirmationMessageLoginMatrixDotOrg
} else if homeserverAddress == "element.io" { } else if homeserverAddress == "element.io" {
return L10n.screenServerConfirmationMessageLoginElementDotIo L10n.screenServerConfirmationMessageLoginElementDotIo
} else { } else {
return "" ""
} }
case .register: 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
} }
} }
} }
@@ -18,14 +18,18 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
} }
init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow) { init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow) {
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: authenticationService.homeserver.value.address, let homeserver = authenticationService.homeserver.value
authenticationFlow: authenticationFlow))
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address,
authenticationFlow: authenticationFlow,
homeserverSupportsRegistration: homeserver.supportsRegistration))
authenticationService.homeserver authenticationService.homeserver
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] homeserver in .sink { [weak self] homeserver in
guard let self else { return } guard let self else { return }
state.homeserverAddress = homeserver.address state.homeserverAddress = homeserver.address
state.homeserverSupportsRegistration = homeserver.supportsRegistration
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
@@ -44,3 +48,9 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
} }
} }
} }
extension LoginHomeserver {
var supportsRegistration: Bool {
loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil)
}
}
@@ -5,6 +5,7 @@
// Please see LICENSE in the repository root for full details. // Please see LICENSE in the repository root for full details.
// //
import Compound
import SwiftUI import SwiftUI
struct ServerConfirmationScreen: View { struct ServerConfirmationScreen: View {
@@ -52,6 +53,7 @@ struct ServerConfirmationScreen: View {
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue) .accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue)
.disabled(!context.viewState.canContinue)
Button { context.send(viewAction: .changeServer) } label: { Button { context.send(viewAction: .changeServer) } label: {
Text(L10n.screenServerConfirmationChangeServer) Text(L10n.screenServerConfirmationChangeServer)
@@ -8,6 +8,10 @@
import Combine import Combine
import SwiftUI import SwiftUI
struct AuthenticationStartScreenParameters {
let webRegistrationEnabled: Bool
}
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol { final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
private var viewModel: AuthenticationStartScreenViewModelProtocol private var viewModel: AuthenticationStartScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AuthenticationStartScreenCoordinatorAction, Never> = .init() private let actionsSubject: PassthroughSubject<AuthenticationStartScreenCoordinatorAction, Never> = .init()
@@ -17,8 +21,8 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init() { init(parameters: AuthenticationStartScreenParameters) {
viewModel = AuthenticationStartScreenViewModel() viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: parameters.webRegistrationEnabled)
} }
// MARK: - Public // MARK: - Public
@@ -33,6 +37,8 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.loginManually) actionsSubject.send(.loginManually)
case .loginWithQR: case .loginWithQR:
actionsSubject.send(.loginWithQR) actionsSubject.send(.loginWithQR)
case .register:
actionsSubject.send(.register)
case .reportProblem: case .reportProblem:
actionsSubject.send(.reportProblem) actionsSubject.send(.reportProblem)
} }
@@ -12,21 +12,25 @@ import SwiftUI
enum AuthenticationStartScreenCoordinatorAction { enum AuthenticationStartScreenCoordinatorAction {
case loginManually case loginManually
case loginWithQR case loginWithQR
case register
case reportProblem case reportProblem
} }
enum AuthenticationStartScreenViewModelAction { enum AuthenticationStartScreenViewModelAction {
case loginManually case loginManually
case loginWithQR case loginWithQR
case register
case reportProblem case reportProblem
} }
struct AuthenticationStartScreenViewState: BindableState { struct AuthenticationStartScreenViewState: BindableState {
var isQRCodeLoginEnabled = false let isWebRegistrationEnabled: Bool
let isQRCodeLoginEnabled: Bool
} }
enum AuthenticationStartScreenViewAction { enum AuthenticationStartScreenViewAction {
case loginManually case loginManually
case loginWithQR case loginWithQR
case register
case reportProblem case reportProblem
} }
@@ -17,9 +17,9 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init() { init(webRegistrationEnabled: Bool) {
super.init(initialViewState: AuthenticationStartScreenViewState()) super.init(initialViewState: AuthenticationStartScreenViewState(isWebRegistrationEnabled: webRegistrationEnabled,
state.isQRCodeLoginEnabled = !ProcessInfo.processInfo.isiOSAppOnMac && AppSettings.isDevelopmentBuild isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac && AppSettings.isDevelopmentBuild))
} }
override func process(viewAction: AuthenticationStartScreenViewAction) { override func process(viewAction: AuthenticationStartScreenViewAction) {
@@ -28,6 +28,8 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
actionsSubject.send(.loginManually) actionsSubject.send(.loginManually)
case .loginWithQR: case .loginWithQR:
actionsSubject.send(.loginWithQR) actionsSubject.send(.loginWithQR)
case .register:
actionsSubject.send(.register)
case .reportProblem: case .reportProblem:
actionsSubject.send(.reportProblem) actionsSubject.send(.reportProblem)
} }
@@ -99,6 +99,14 @@ struct AuthenticationStartScreen: View {
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signIn) .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) .padding(.horizontal, verticalSizeClass == .compact ? 128 : 24)
.readableFrame() .readableFrame()
@@ -108,7 +116,7 @@ struct AuthenticationStartScreen: View {
// MARK: - Previews // MARK: - Previews
struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview { struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = AuthenticationStartScreenViewModel() static let viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: true)
static var previews: some View { static var previews: some View {
AuthenticationStartScreen(context: viewModel.context) AuthenticationStartScreen(context: viewModel.context)
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:48fb262e42f8d01a6feeb3e4e70b72afd11e57469c87c1fb2a649e58967f0fef oid sha256:acb92bfaa2a8849b1debe3852d55dcdb2eec2d77e1aaf2417c75248ae2538b2c
size 1257762 size 1260707
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:028e7ad09b78fad84301635102b9968fb2e6d957a85f2b49a5be2d81fff0e9f3 oid sha256:2ce2c744aa3b516479a4bbac4d69f9c324f736200330cd8d9107deceed604696
size 1287670 size 1294329
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dc44d147264a7965e37c72ea2a9656565d5919b1fceeeb75c4bbf7aa3a320300 oid sha256:b4bfbf0ff45d25c9f6925fe2669444bcdb6a3552c8c4d92855e47bf99bb181fd
size 530595 size 511521
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:204ba84c66920eaee46b314f64d035ce834cd08e22aaa0c9f0d0b58a6db07bd0 oid sha256:6e044f8265712ff0ebe207d32eedc5f9ff93ede67e398ea5fa9bb5339624584a
size 557370 size 535038
@@ -25,12 +25,19 @@ class ServerConfirmationScreenViewStateTests: XCTestCase {
} }
func testRegisterMessageString() { func testRegisterMessageString() {
let matrixDotOrgLogin = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address, let matrixDotOrgRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address,
authenticationFlow: .register) authenticationFlow: .register,
XCTAssertEqual(matrixDotOrgLogin.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") homeserverSupportsRegistration: true)
XCTAssertEqual(matrixDotOrgRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
let otherLogin = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address, let oidcRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address,
authenticationFlow: .register) authenticationFlow: .register,
XCTAssertEqual(otherLogin.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") 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.")
} }
} }