From 6efbf6117f5b1dcdff13e50f8b398059e6847161 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:05:22 +0100 Subject: [PATCH] Allow registration on matrix.org using a custom helper URL. (#3282) --- .../Sources/Application/AppSettings.swift | 3 + .../AuthenticationFlowCoordinator.swift | 56 ++++++++++++++----- .../LoginScreen/LoginHomeserver.swift | 5 +- .../ServerConfirmationScreenModels.swift | 22 ++++++-- .../ServerConfirmationScreenViewModel.swift | 14 ++++- .../View/ServerConfirmationScreen.swift | 2 + ...AuthenticationStartScreenCoordinator.swift | 10 +++- .../AuthenticationStartScreenModels.swift | 6 +- .../AuthenticationStartScreenViewModel.swift | 8 ++- .../View/AuthenticationStartScreen.swift | 10 +++- ...authenticationStartScreen-iPad-en-GB.1.png | 4 +- ...uthenticationStartScreen-iPad-pseudo.1.png | 4 +- ...nticationStartScreen-iPhone-15-en-GB.1.png | 4 +- ...ticationStartScreen-iPhone-15-pseudo.1.png | 4 +- ...verConfigurationScreenViewStateTests.swift | 19 +++++-- 15 files changed, 129 insertions(+), 42 deletions(-) diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 8dc6e2d17..4ecc85783 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -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 { diff --git a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift index b463f0ee9..996ce5b74 100644 --- a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift @@ -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() diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift index 7f6695c8a..5bfb33da7 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -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. diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift index bddaaf21b..d456b9eba 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift @@ -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 } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift index d0dadfa73..717c8f76f 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift @@ -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) + } +} diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift index 53aa1c4de..78fbed428 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift @@ -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) diff --git a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenCoordinator.swift b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenCoordinator.swift index 24683d7c6..c07bb65fc 100644 --- a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenCoordinator.swift +++ b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenCoordinator.swift @@ -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 = .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) } diff --git a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenModels.swift b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenModels.swift index 2f7078980..3ae56e871 100644 --- a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenModels.swift +++ b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenModels.swift @@ -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 } diff --git a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift index 76f8a7da0..10f9b1610 100644 --- a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift +++ b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift @@ -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) } diff --git a/ElementX/Sources/Screens/AuthenticationStartScreen/View/AuthenticationStartScreen.swift b/ElementX/Sources/Screens/AuthenticationStartScreen/View/AuthenticationStartScreen.swift index 1aada6902..7871b47f3 100644 --- a/ElementX/Sources/Screens/AuthenticationStartScreen/View/AuthenticationStartScreen.swift +++ b/ElementX/Sources/Screens/AuthenticationStartScreen/View/AuthenticationStartScreen.swift @@ -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) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-en-GB.1.png index 847aaddfe..7c5df27f3 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48fb262e42f8d01a6feeb3e4e70b72afd11e57469c87c1fb2a649e58967f0fef -size 1257762 +oid sha256:acb92bfaa2a8849b1debe3852d55dcdb2eec2d77e1aaf2417c75248ae2538b2c +size 1260707 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-pseudo.1.png index 431c408c1..7485551f1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:028e7ad09b78fad84301635102b9968fb2e6d957a85f2b49a5be2d81fff0e9f3 -size 1287670 +oid sha256:2ce2c744aa3b516479a4bbac4d69f9c324f736200330cd8d9107deceed604696 +size 1294329 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-en-GB.1.png index e647d71d0..731743311 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc44d147264a7965e37c72ea2a9656565d5919b1fceeeb75c4bbf7aa3a320300 -size 530595 +oid sha256:b4bfbf0ff45d25c9f6925fe2669444bcdb6a3552c8c4d92855e47bf99bb181fd +size 511521 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-pseudo.1.png index 9a7afc580..88a7cc505 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:204ba84c66920eaee46b314f64d035ce834cd08e22aaa0c9f0d0b58a6db07bd0 -size 557370 +oid sha256:6e044f8265712ff0ebe207d32eedc5f9ff93ede67e398ea5fa9bb5339624584a +size 535038 diff --git a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift index 3c52cd9a6..b859e1741 100644 --- a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift +++ b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift @@ -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.") } }