mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Allow registration on matrix.org using a custom helper URL. (#3282)
This commit is contained in:
parent
6cfe09b96d
commit
6efbf6117f
@ -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 {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_authenticationStartScreen-iPhone-15-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
@ -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.")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user