mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Simplify authentication logic
matrix.org now supports MAS so we don't need the web registration helper anymore.
This commit is contained in:
@@ -210,8 +210,10 @@ final class AppSettings {
|
||||
contacts: [supportEmailAddress],
|
||||
staticRegistrations: oidcStaticRegistrations.mapKeys { $0.absoluteString })
|
||||
|
||||
/// A temporary hack to allow registration on matrix.org until MAS is deployed.
|
||||
let webRegistrationEnabled = true
|
||||
/// Whether or not the Create Account button is shown on the start screen.
|
||||
///
|
||||
/// **Note:** Setting this to false doesn't prevent someone from creating an account when the selected homeserver's MAS allows registration.
|
||||
let showCreateAccountButton = true
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// MARK: - Private
|
||||
|
||||
private func showStartScreen() {
|
||||
let parameters = AuthenticationStartScreenParameters(webRegistrationEnabled: appSettings.webRegistrationEnabled,
|
||||
let parameters = AuthenticationStartScreenParameters(showCreateAccountButton: appSettings.showCreateAccountButton,
|
||||
isBugReportServiceEnabled: bugReportService.isEnabled)
|
||||
let coordinator = AuthenticationStartScreenCoordinator(parameters: parameters)
|
||||
|
||||
@@ -145,10 +145,8 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .continue(let window):
|
||||
if authenticationService.homeserver.value.loginMode == .oidc, let window {
|
||||
if authenticationService.homeserver.value.loginMode.supportsOIDCFlow, let window {
|
||||
showOIDCAuthentication(presentationAnchor: window)
|
||||
} else if authenticationFlow == .register {
|
||||
showWebRegistration()
|
||||
} else {
|
||||
showLoginScreen()
|
||||
}
|
||||
@@ -187,26 +185,6 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -15,19 +15,21 @@ extension AuthenticationClientBuilderMock {
|
||||
"example.com": ClientSDKMock(configuration: .init(serverAddress: "example.com",
|
||||
homeserverURL: "https://matrix.example.com",
|
||||
slidingSyncVersion: .native,
|
||||
supportsPasswordLogin: true,
|
||||
elementWellKnown: "")),
|
||||
oidcLoginURL: nil,
|
||||
supportsOIDCCreatePrompt: false,
|
||||
supportsPasswordLogin: true)),
|
||||
"company.com": ClientSDKMock(configuration: .init(serverAddress: "company.com",
|
||||
homeserverURL: "https://matrix.company.com",
|
||||
slidingSyncVersion: .native,
|
||||
oidcLoginURL: "https://auth.company.com/oidc",
|
||||
supportsPasswordLogin: false,
|
||||
elementWellKnown: "")),
|
||||
supportsOIDCCreatePrompt: false,
|
||||
supportsPasswordLogin: false)),
|
||||
"server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net",
|
||||
homeserverURL: "https://matrix.example.com",
|
||||
slidingSyncVersion: .native,
|
||||
supportsPasswordLogin: false,
|
||||
elementWellKnown: ""))
|
||||
oidcLoginURL: nil,
|
||||
supportsOIDCCreatePrompt: false,
|
||||
supportsPasswordLogin: false))
|
||||
]
|
||||
var qrCodeClient = ClientSDKMock(configuration: .init())
|
||||
}
|
||||
|
||||
@@ -76,8 +76,6 @@ extension ClientProxyMock {
|
||||
recentlyVisitedRoomsReturnValue = .success([])
|
||||
recentConversationCounterpartsReturnValue = []
|
||||
|
||||
getElementWellKnownReturnValue = .success(nil)
|
||||
|
||||
loadMediaContentForSourceThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
|
||||
loadMediaThumbnailForSourceWidthHeightThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
|
||||
loadMediaFileForSourceFilenameThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
|
||||
|
||||
@@ -4281,70 +4281,6 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
return isAliasAvailableReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - getElementWellKnown
|
||||
|
||||
var getElementWellKnownUnderlyingCallsCount = 0
|
||||
var getElementWellKnownCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return getElementWellKnownUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = getElementWellKnownUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
getElementWellKnownUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
getElementWellKnownUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var getElementWellKnownCalled: Bool {
|
||||
return getElementWellKnownCallsCount > 0
|
||||
}
|
||||
|
||||
var getElementWellKnownUnderlyingReturnValue: Result<ElementWellKnown?, ClientProxyError>!
|
||||
var getElementWellKnownReturnValue: Result<ElementWellKnown?, ClientProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return getElementWellKnownUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<ElementWellKnown?, ClientProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = getElementWellKnownUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
getElementWellKnownUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
getElementWellKnownUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var getElementWellKnownClosure: (() async -> Result<ElementWellKnown?, ClientProxyError>)?
|
||||
|
||||
func getElementWellKnown() async -> Result<ElementWellKnown?, ClientProxyError> {
|
||||
getElementWellKnownCallsCount += 1
|
||||
if let getElementWellKnownClosure = getElementWellKnownClosure {
|
||||
return await getElementWellKnownClosure()
|
||||
} else {
|
||||
return getElementWellKnownReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - clearCaches
|
||||
|
||||
var clearCachesUnderlyingCallsCount = 0
|
||||
|
||||
@@ -15,9 +15,9 @@ extension ClientSDKMock {
|
||||
var serverAddress = "matrix.org"
|
||||
var homeserverURL = "https://matrix-client.matrix.org"
|
||||
var slidingSyncVersion = SlidingSyncVersion.native
|
||||
var oidcLoginURL: String?
|
||||
var oidcLoginURL: String? = "https://account.matrix.org/authorize"
|
||||
var supportsOIDCCreatePrompt = true
|
||||
var supportsPasswordLogin = true
|
||||
var elementWellKnown = "{\"registration_helper_url\":\"https://develop.element.io/#/mobile_register\"}"
|
||||
var validCredentials = (username: "alice", password: "12345678")
|
||||
|
||||
// MARK: Session
|
||||
@@ -41,7 +41,6 @@ extension ClientSDKMock {
|
||||
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
|
||||
userIdServerNameThrowableError = MockError.generic
|
||||
serverReturnValue = "https://\(configuration.serverAddress)"
|
||||
getUrlUrlReturnValue = configuration.elementWellKnown
|
||||
urlForOidcOidcConfigurationPromptReturnValue = OAuthAuthorizationDataSDKMock(configuration: configuration)
|
||||
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in
|
||||
guard username == configuration.validCredentials.username,
|
||||
@@ -62,6 +61,11 @@ extension HomeserverLoginDetailsSDKMock {
|
||||
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
|
||||
supportsPasswordLoginReturnValue = configuration.supportsPasswordLogin
|
||||
supportsOidcLoginReturnValue = configuration.oidcLoginURL != nil
|
||||
supportedOidcPromptsReturnValue = switch (configuration.oidcLoginURL, configuration.supportsOIDCCreatePrompt) {
|
||||
case (.none, _): []
|
||||
case (.some, true): [.consent, .create]
|
||||
case (.some, false): [.consent]
|
||||
}
|
||||
urlReturnValue = configuration.homeserverURL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,13 @@ struct LoginHomeserver: Equatable {
|
||||
let address: String
|
||||
/// The types login supported by the homeserver.
|
||||
var loginMode: LoginMode
|
||||
/// A temporary helper URL that can be used for registration.
|
||||
var registrationHelperURL: URL?
|
||||
|
||||
/// Creates a new homeserver value.
|
||||
init(address: String, loginMode: LoginMode, registrationHelperURL: URL? = nil) {
|
||||
init(address: String, loginMode: LoginMode) {
|
||||
let address = Self.sanitized(address).components(separatedBy: "://").last ?? address
|
||||
|
||||
self.address = address
|
||||
self.loginMode = loginMode
|
||||
self.registrationHelperURL = registrationHelperURL
|
||||
}
|
||||
|
||||
/// Whether or not the app is able to register on this homeserver.
|
||||
var supportsRegistration: Bool {
|
||||
loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil)
|
||||
}
|
||||
|
||||
/// Sanitizes a user entered homeserver address with the following rules
|
||||
@@ -53,7 +45,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, registrationHelperURL: "https://develop.element.io/#/mobile_register")
|
||||
LoginHomeserver(address: "matrix.org", loginMode: .oidc(supportsCreatePrompt: true))
|
||||
}
|
||||
|
||||
/// A mock homeserver that supports login and registration via a password but has no SSO providers.
|
||||
@@ -63,7 +55,7 @@ extension LoginHomeserver {
|
||||
|
||||
/// A mock homeserver that supports only supports authentication via a single SSO provider.
|
||||
static var mockOIDC: LoginHomeserver {
|
||||
LoginHomeserver(address: "company.com", loginMode: .oidc)
|
||||
LoginHomeserver(address: "company.com", loginMode: .oidc(supportsCreatePrompt: false))
|
||||
}
|
||||
|
||||
/// A mock homeserver that only with no supported login flows.
|
||||
|
||||
@@ -12,7 +12,7 @@ enum LoginMode: Equatable {
|
||||
/// The login mode hasn't been determined yet.
|
||||
case unknown
|
||||
/// The homeserver supports login via OpenID Connect.
|
||||
case oidc
|
||||
case oidc(supportsCreatePrompt: Bool)
|
||||
/// The homeserver supports login with a password.
|
||||
case password
|
||||
/// The homeserver only allows login with unsupported mechanisms. Use fallback instead.
|
||||
|
||||
@@ -69,7 +69,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
||||
Task {
|
||||
switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
|
||||
case .success:
|
||||
if authenticationService.homeserver.value.loginMode == .oidc {
|
||||
if authenticationService.homeserver.value.loginMode.supportsOIDCFlow {
|
||||
actionsSubject.send(.configuredForOIDC)
|
||||
}
|
||||
stopLoading()
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct WebRegistrationScreen: View {
|
||||
@ObservedObject var context: WebRegistrationScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
WebRegistrationWebView(url: context.viewState.url, viewModelContext: context)
|
||||
.navigationTitle(L10n.screenCreateAccountTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.visible, for: .navigationBar)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebRegistrationWebView: UIViewRepresentable {
|
||||
let url: URL
|
||||
let viewModelContext: WebRegistrationScreenViewModel.Context
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
context.coordinator.webView
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) { }
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(url: url, viewModelContext: viewModelContext)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, WKUIDelegate {
|
||||
private let url: URL
|
||||
private let viewModelContext: WebRegistrationScreenViewModel.Context
|
||||
|
||||
private(set) var webView: WKWebView!
|
||||
|
||||
init(url: URL, viewModelContext: WebRegistrationScreenViewModel.Context) {
|
||||
self.url = url
|
||||
self.viewModelContext = viewModelContext
|
||||
|
||||
super.init()
|
||||
|
||||
let eventHandlerName = "elementx"
|
||||
let userContentController = WKUserContentController()
|
||||
userContentController.add(WKScriptMessageHandlerWrapper(self), name: eventHandlerName)
|
||||
|
||||
let eventHandlerScript = """
|
||||
window.addEventListener(
|
||||
"mobileregistrationresponse",
|
||||
(event) => {
|
||||
window.webkit.messageHandlers.\(eventHandlerName).postMessage(JSON.stringify(event.detail));
|
||||
},
|
||||
false,
|
||||
);
|
||||
"""
|
||||
|
||||
let userScript = WKUserScript(source: eventHandlerScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
|
||||
userContentController.addUserScript(userScript)
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.userContentController = userContentController
|
||||
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
||||
|
||||
webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
webView.uiDelegate = self
|
||||
webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage) {
|
||||
guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else {
|
||||
MXLog.error("Unexpected response.")
|
||||
return
|
||||
}
|
||||
|
||||
guard let credentials = try? JSONDecoder().decode(WebRegistrationCredentials.self, from: jsonData) else {
|
||||
MXLog.error("Invalid response.")
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.info("Received login credentials.")
|
||||
viewModelContext.send(viewAction: .signedIn(credentials))
|
||||
}
|
||||
|
||||
// MARK: WKUIDelegate
|
||||
|
||||
func webView(_ webView: WKWebView,
|
||||
createWebViewWith configuration: WKWebViewConfiguration,
|
||||
for navigationAction: WKNavigationAction,
|
||||
windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
if let url = navigationAction.request.url, UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Avoids retain loops between the configuration and webView coordinator
|
||||
private class WKScriptMessageHandlerWrapper: NSObject, WKScriptMessageHandler {
|
||||
private weak var coordinator: Coordinator?
|
||||
|
||||
init(_ coordinator: Coordinator) {
|
||||
self.coordinator = coordinator
|
||||
}
|
||||
|
||||
// MARK: WKScriptMessageHandler
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage) {
|
||||
coordinator?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct WebRegistrationScreen_Previews: PreviewProvider {
|
||||
static let viewModel = WebRegistrationScreenViewModel(registrationHelperURL: "https://develop.element.io/#/mobile_register")
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
WebRegistrationScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct WebRegistrationScreenCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum WebRegistrationScreenCoordinatorAction: CustomStringConvertible {
|
||||
case cancel
|
||||
case signedIn(UserSessionProtocol)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .cancel: "cancel"
|
||||
case .signedIn: "signedIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This code was based on the LoginScreen, we should move the authentication service logic into the view model.
|
||||
final class WebRegistrationScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: WebRegistrationScreenCoordinatorParameters
|
||||
private let viewModel: WebRegistrationScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<WebRegistrationScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<WebRegistrationScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: WebRegistrationScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
guard let registrationHelperURL = parameters.authenticationService.homeserver.value.registrationHelperURL else {
|
||||
MXLog.error("Attempted registration without a helper URL.")
|
||||
fatalError("A helper URL is required.")
|
||||
}
|
||||
viewModel = WebRegistrationScreenViewModel(registrationHelperURL: registrationHelperURL)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
case .signedIn(let credentials):
|
||||
Task { await self.completeRegistration(using: credentials) }
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(WebRegistrationScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func completeRegistration(using credentials: WebRegistrationCredentials) async {
|
||||
switch await parameters.authenticationService.completeWebRegistration(using: credentials) {
|
||||
case .success(let userSession):
|
||||
actionsSubject.send(.signedIn(userSession))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed registration: \(error)")
|
||||
parameters.userIndicatorController.alertInfo = .init(id: UUID(), title: L10n.errorUnknown, message: String(describing: error))
|
||||
}
|
||||
}
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum WebRegistrationScreenViewModelAction: CustomStringConvertible {
|
||||
case cancel
|
||||
case signedIn(WebRegistrationCredentials)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .cancel: "cancel"
|
||||
case .signedIn: "signedIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebRegistrationScreenViewState: BindableState {
|
||||
var url: URL
|
||||
var bindings = WebRegistrationScreenViewStateBindings()
|
||||
}
|
||||
|
||||
struct WebRegistrationScreenViewStateBindings { }
|
||||
|
||||
enum WebRegistrationScreenViewAction: CustomStringConvertible {
|
||||
case cancel
|
||||
case signedIn(WebRegistrationCredentials)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .cancel: "cancel"
|
||||
case .signedIn: "signedIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebRegistrationCredentials: Decodable {
|
||||
let userID: String
|
||||
let accessToken: String
|
||||
let deviceID: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case userID = "user_id"
|
||||
case accessToken = "access_token"
|
||||
case deviceID = "device_id"
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
typealias WebRegistrationScreenViewModelType = StateStoreViewModel<WebRegistrationScreenViewState, WebRegistrationScreenViewAction>
|
||||
|
||||
class WebRegistrationScreenViewModel: WebRegistrationScreenViewModelType, WebRegistrationScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<WebRegistrationScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<WebRegistrationScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(registrationHelperURL: URL) {
|
||||
super.init(initialViewState: WebRegistrationScreenViewState(url: registrationHelperURL))
|
||||
}
|
||||
|
||||
override func process(viewAction: WebRegistrationScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
case .signedIn(let credentials):
|
||||
actionsSubject.send(.signedIn(credentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol WebRegistrationScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<WebRegistrationScreenViewModelAction, Never> { get }
|
||||
var context: WebRegistrationScreenViewModelType.Context { get }
|
||||
}
|
||||
+2
-2
@@ -9,7 +9,7 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct AuthenticationStartScreenParameters {
|
||||
let webRegistrationEnabled: Bool
|
||||
let showCreateAccountButton: Bool
|
||||
let isBugReportServiceEnabled: Bool
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(parameters: AuthenticationStartScreenParameters) {
|
||||
viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: parameters.webRegistrationEnabled,
|
||||
viewModel = AuthenticationStartScreenViewModel(showCreateAccountButton: parameters.showCreateAccountButton,
|
||||
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ enum AuthenticationStartScreenViewModelAction {
|
||||
}
|
||||
|
||||
struct AuthenticationStartScreenViewState: BindableState {
|
||||
let isWebRegistrationEnabled: Bool
|
||||
let showCreateAccountButton: Bool
|
||||
let isQRCodeLoginEnabled: Bool
|
||||
let isBugReportServiceEnabled: Bool
|
||||
}
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(webRegistrationEnabled: Bool, isBugReportServiceEnabled: Bool) {
|
||||
super.init(initialViewState: AuthenticationStartScreenViewState(isWebRegistrationEnabled: webRegistrationEnabled,
|
||||
init(showCreateAccountButton: Bool, isBugReportServiceEnabled: Bool) {
|
||||
super.init(initialViewState: AuthenticationStartScreenViewState(showCreateAccountButton: showCreateAccountButton,
|
||||
isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac,
|
||||
isBugReportServiceEnabled: isBugReportServiceEnabled))
|
||||
}
|
||||
|
||||
+2
-2
@@ -102,7 +102,7 @@ struct AuthenticationStartScreen: View {
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signIn)
|
||||
|
||||
if context.viewState.isWebRegistrationEnabled {
|
||||
if context.viewState.showCreateAccountButton {
|
||||
Button { context.send(viewAction: .register) } label: {
|
||||
Text(L10n.screenCreateAccountTitle)
|
||||
.padding(14)
|
||||
@@ -129,7 +129,7 @@ struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
|
||||
}
|
||||
|
||||
static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> AuthenticationStartScreenViewModel {
|
||||
AuthenticationStartScreenViewModel(webRegistrationEnabled: true,
|
||||
AuthenticationStartScreenViewModel(showCreateAccountButton: true,
|
||||
isBugReportServiceEnabled: isBugReportServiceEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,6 @@ private struct CallView: UIViewRepresentable {
|
||||
struct CallScreen_Previews: PreviewProvider {
|
||||
static let viewModel = {
|
||||
let clientProxy = ClientProxyMock()
|
||||
clientProxy.getElementWellKnownReturnValue = .success(nil)
|
||||
clientProxy.deviceID = "call-device-id"
|
||||
|
||||
let roomProxy = JoinedRoomProxyMock()
|
||||
|
||||
@@ -48,27 +48,21 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
|
||||
let client = try await makeClientBuilder().build(homeserverAddress: homeserverAddress)
|
||||
let loginDetails = await client.homeserverLoginDetails()
|
||||
let elementWellKnown = await client.getElementWellKnown()
|
||||
|
||||
MXLog.info("Sliding sync: \(client.slidingSyncVersion())")
|
||||
|
||||
homeserver.loginMode = if loginDetails.supportsOidcLogin() {
|
||||
.oidc
|
||||
.oidc(supportsCreatePrompt: loginDetails.supportedOidcPrompts().contains(.create))
|
||||
} else if loginDetails.supportsPasswordLogin() {
|
||||
.password
|
||||
} else {
|
||||
.unsupported
|
||||
}
|
||||
|
||||
homeserver.registrationHelperURL = switch elementWellKnown {
|
||||
case .success(let wellKnown): wellKnown.registrationHelperUrl.flatMap(URL.init)
|
||||
case .failure: nil
|
||||
}
|
||||
|
||||
if flow == .login, homeserver.loginMode == .unsupported {
|
||||
return .failure(.loginNotSupported)
|
||||
}
|
||||
if flow == .register, !homeserver.supportsRegistration {
|
||||
if flow == .register, !homeserver.loginMode.supportsOIDCFlow {
|
||||
return .failure(.registrationNotSupported)
|
||||
}
|
||||
|
||||
@@ -91,6 +85,8 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> {
|
||||
guard let client else { return .failure(.oidcError(.urlFailure)) }
|
||||
do {
|
||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||
// let prompt: OidcPrompt = flow == .register ? .create : .consent
|
||||
let oidcData = try await client.urlForOidc(oidcConfiguration: appSettings.oidcConfiguration.rustValue,
|
||||
prompt: .consent)
|
||||
return .success(OIDCAuthorizationDataProxy(underlyingData: oidcData))
|
||||
@@ -148,25 +144,6 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result<any UserSessionProtocol, AuthenticationServiceError> {
|
||||
guard let client else { return .failure(.failedLoggingIn) }
|
||||
let session = Session(accessToken: credentials.accessToken,
|
||||
refreshToken: nil,
|
||||
userId: credentials.userID,
|
||||
deviceId: credentials.deviceID,
|
||||
homeserverUrl: client.homeserver(),
|
||||
oidcData: nil,
|
||||
slidingSyncVersion: client.slidingSyncVersion())
|
||||
|
||||
do {
|
||||
try await client.restoreSession(session: session)
|
||||
return await userSession(for: client)
|
||||
} catch {
|
||||
MXLog.error("Failed restoring the client using the provided credentials.")
|
||||
return .failure(.failedUsingWebCredentials)
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
homeserverSubject.send(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown))
|
||||
flow = .login
|
||||
|
||||
@@ -48,8 +48,6 @@ protocol AuthenticationServiceProtocol {
|
||||
func loginWithOIDCCallback(_ callbackURL: URL) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
/// Performs a password login using the current homeserver.
|
||||
func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
/// Completes registration using the credentials obtained via the helper URL.
|
||||
func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
|
||||
/// Resets the current configuration requiring `configure(for:flow:)` to be called again.
|
||||
func reset()
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension ClientProtocol {
|
||||
func getElementWellKnown() async -> Result<MatrixRustSDK.ElementWellKnown, ClientProxyError> {
|
||||
do {
|
||||
let serverName = if let userIDServerName = try? userIdServerName() {
|
||||
"https://\(userIDServerName)"
|
||||
} else {
|
||||
server()
|
||||
}
|
||||
|
||||
guard let serverName,
|
||||
let url = URL(string: serverName)?.appending(path: "/.well-known/element/element.json") else {
|
||||
return .failure(.invalidServerName)
|
||||
}
|
||||
|
||||
let response = try await getUrl(url: url.absoluteString)
|
||||
let wellKnown = try makeElementWellKnown(string: response)
|
||||
return .success(wellKnown)
|
||||
} catch {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,10 +703,6 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func getElementWellKnown() async -> Result<ElementWellKnown?, ClientProxyError> {
|
||||
await client.getElementWellKnown().map(ElementWellKnown.init)
|
||||
}
|
||||
|
||||
func clearCaches() async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
return try await .success(client.clearCaches())
|
||||
|
||||
@@ -182,8 +182,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func isAliasAvailable(_ alias: String) async -> Result<Bool, ClientProxyError>
|
||||
|
||||
func getElementWellKnown() async -> Result<ElementWellKnown?, ClientProxyError>
|
||||
|
||||
@discardableResult func clearCaches() async -> Result<Void, ClientProxyError>
|
||||
|
||||
// MARK: - Ignored users
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct ElementWellKnown {
|
||||
let registrationHelperURL: URL?
|
||||
|
||||
init?(_ wellKnown: MatrixRustSDK.ElementWellKnown) {
|
||||
registrationHelperURL = wellKnown.registrationHelperUrl.flatMap(URL.init)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user