mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Configure the AuthenticationService later now that we have 2 flows on the start screen. (#3316)
* Don't query the homeserver until confirming it (or selecting a different one). * Setup the infrastructure to test AuthenticationService. Implement basic tests for configuration & password login. * Use the real AuthenticationService with a mock Client in all of the tests. * Add tests for the ServerConfirmationScreenViewModel. * Remove redundant view state and test for it.
This commit is contained in:
@@ -485,7 +485,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
_ = await authenticationService.configure(for: userSession.clientProxy.homeserver)
|
||||
_ = await authenticationService.configure(for: userSession.clientProxy.homeserver, flow: .login)
|
||||
|
||||
let parameters = SoftLogoutScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
credentials: credentials,
|
||||
|
||||
@@ -86,11 +86,11 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .loginManually:
|
||||
Task { await self.startAuthentication(flow: .login) }
|
||||
showServerConfirmationScreen(authenticationFlow: .login)
|
||||
case .loginWithQR:
|
||||
startQRCodeLogin()
|
||||
case .register:
|
||||
Task { await self.startAuthentication(flow: .register) }
|
||||
showServerConfirmationScreen(authenticationFlow: .register)
|
||||
case .reportProblem:
|
||||
showReportProblemScreen()
|
||||
}
|
||||
@@ -113,7 +113,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch action {
|
||||
case .signInManually:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
Task { await self.startAuthentication(flow: .login) }
|
||||
showServerConfirmationScreen(authenticationFlow: .login)
|
||||
case .cancel:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .done(let userSession):
|
||||
@@ -137,25 +137,14 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
bugReportFlowCoordinator?.start()
|
||||
}
|
||||
|
||||
private func startAuthentication(flow: AuthenticationFlow) async {
|
||||
startLoading()
|
||||
|
||||
switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) {
|
||||
case .success:
|
||||
stopLoading()
|
||||
showServerConfirmationScreen(authenticationFlow: flow)
|
||||
case .failure:
|
||||
stopLoading()
|
||||
showServerSelectionScreen(authenticationFlow: flow, isModallyPresented: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow, isModallyPresented: Bool) {
|
||||
// TODO: Move this method after showServerConfirmationScreen
|
||||
private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow) {
|
||||
let navigationCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
userIndicatorController: userIndicatorController,
|
||||
isModallyPresented: isModallyPresented)
|
||||
authenticationFlow: authenticationFlow,
|
||||
slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: userIndicatorController)
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions
|
||||
@@ -164,42 +153,26 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .updated:
|
||||
if isModallyPresented {
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
} else {
|
||||
// We are here because the default server failed to respond.
|
||||
if authenticationService.homeserver.value.loginMode == .password {
|
||||
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(authenticationFlow: authenticationFlow)
|
||||
}
|
||||
}
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if isModallyPresented {
|
||||
navigationCoordinator.setRootCoordinator(coordinator)
|
||||
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
|
||||
} else {
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
navigationCoordinator.setRootCoordinator(coordinator)
|
||||
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
|
||||
}
|
||||
|
||||
private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) {
|
||||
// Reset the service back to the default homeserver before continuing. This ensures
|
||||
// we check that registration is supported if it was previously configured for login.
|
||||
authenticationService.reset()
|
||||
|
||||
let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
authenticationFlow: authenticationFlow)
|
||||
authenticationFlow: authenticationFlow,
|
||||
slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: userIndicatorController)
|
||||
let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
@@ -215,7 +188,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
showLoginScreen()
|
||||
}
|
||||
case .changeServer:
|
||||
showServerSelectionScreen(authenticationFlow: authenticationFlow, isModallyPresented: true)
|
||||
showServerSelectionScreen(authenticationFlow: authenticationFlow)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension AuthenticationClientBuilderFactoryMock {
|
||||
struct Configuration {
|
||||
var builderConfiguration: AuthenticationClientBuilderMock.Configuration = .init()
|
||||
}
|
||||
|
||||
convenience init(configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
let clientBuilder = AuthenticationClientBuilderMock(configuration: configuration.builderConfiguration)
|
||||
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue = clientBuilder
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension AuthenticationClientBuilderMock {
|
||||
struct Configuration {
|
||||
var homeserverClients = [
|
||||
"matrix.org": ClientSDKMock(configuration: .init()),
|
||||
"example.com": ClientSDKMock(configuration: .init(serverAddress: "example.com",
|
||||
homeserverURL: "https://matrix.example.com",
|
||||
slidingSyncVersion: .native,
|
||||
supportsPasswordLogin: true,
|
||||
elementWellKnown: "")),
|
||||
"company.com": ClientSDKMock(configuration: .init(serverAddress: "company.com",
|
||||
homeserverURL: "https://matrix.company.com",
|
||||
slidingSyncVersion: .native,
|
||||
oidcLoginURL: "https://auth.company.com/oidc",
|
||||
supportsPasswordLogin: false,
|
||||
elementWellKnown: "")),
|
||||
"server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net",
|
||||
homeserverURL: "https://matrix.example.com",
|
||||
slidingSyncVersion: .native,
|
||||
supportsPasswordLogin: false,
|
||||
elementWellKnown: ""))
|
||||
]
|
||||
var qrCodeClient = ClientSDKMock(configuration: .init())
|
||||
}
|
||||
|
||||
convenience init(configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
buildHomeserverAddressClosure = { address in
|
||||
guard let client = configuration.homeserverClients[address] else {
|
||||
throw ClientBuildError.ServerUnreachable(message: "Not a known homeserver.")
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue = configuration.qrCodeClient
|
||||
}
|
||||
}
|
||||
@@ -1819,6 +1819,230 @@ class AudioSessionMock: AudioSessionProtocol {
|
||||
try setActiveOptionsClosure?(active, options)
|
||||
}
|
||||
}
|
||||
class AuthenticationClientBuilderFactoryMock: AuthenticationClientBuilderFactoryProtocol {
|
||||
|
||||
//MARK: - makeBuilder
|
||||
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCalled: Bool {
|
||||
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount > 0
|
||||
}
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments: (sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)?
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = []
|
||||
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: AuthenticationClientBuilderProtocol!
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue: AuthenticationClientBuilderProtocol! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: AuthenticationClientBuilderProtocol? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure: ((SessionDirectories, String, ClientSessionDelegate, AppSettings, AppHooks) -> AuthenticationClientBuilderProtocol)?
|
||||
|
||||
func makeBuilder(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) -> AuthenticationClientBuilderProtocol {
|
||||
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount += 1
|
||||
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments = (sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)
|
||||
DispatchQueue.main.async {
|
||||
self.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations.append((sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks))
|
||||
}
|
||||
if let makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure {
|
||||
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure(sessionDirectories, passphrase, clientSessionDelegate, appSettings, appHooks)
|
||||
} else {
|
||||
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class AuthenticationClientBuilderMock: AuthenticationClientBuilderProtocol {
|
||||
|
||||
//MARK: - build
|
||||
|
||||
var buildHomeserverAddressThrowableError: Error?
|
||||
var buildHomeserverAddressUnderlyingCallsCount = 0
|
||||
var buildHomeserverAddressCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return buildHomeserverAddressUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = buildHomeserverAddressUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
buildHomeserverAddressUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
buildHomeserverAddressUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var buildHomeserverAddressCalled: Bool {
|
||||
return buildHomeserverAddressCallsCount > 0
|
||||
}
|
||||
var buildHomeserverAddressReceivedHomeserverAddress: String?
|
||||
var buildHomeserverAddressReceivedInvocations: [String] = []
|
||||
|
||||
var buildHomeserverAddressUnderlyingReturnValue: ClientProtocol!
|
||||
var buildHomeserverAddressReturnValue: ClientProtocol! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return buildHomeserverAddressUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: ClientProtocol? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = buildHomeserverAddressUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
buildHomeserverAddressUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
buildHomeserverAddressUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var buildHomeserverAddressClosure: ((String) async throws -> ClientProtocol)?
|
||||
|
||||
func build(homeserverAddress: String) async throws -> ClientProtocol {
|
||||
if let error = buildHomeserverAddressThrowableError {
|
||||
throw error
|
||||
}
|
||||
buildHomeserverAddressCallsCount += 1
|
||||
buildHomeserverAddressReceivedHomeserverAddress = homeserverAddress
|
||||
DispatchQueue.main.async {
|
||||
self.buildHomeserverAddressReceivedInvocations.append(homeserverAddress)
|
||||
}
|
||||
if let buildHomeserverAddressClosure = buildHomeserverAddressClosure {
|
||||
return try await buildHomeserverAddressClosure(homeserverAddress)
|
||||
} else {
|
||||
return buildHomeserverAddressReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - buildWithQRCode
|
||||
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError: Error?
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = 0
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCalled: Bool {
|
||||
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount > 0
|
||||
}
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments: (qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)?
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations: [(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)] = []
|
||||
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue: ClientProtocol!
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue: ClientProtocol! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: ClientProtocol? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure: ((QrCodeData, OIDCConfigurationProxy, QrLoginProgressListenerProxy) async throws -> ClientProtocol)?
|
||||
|
||||
func buildWithQRCode(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol {
|
||||
if let error = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError {
|
||||
throw error
|
||||
}
|
||||
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount += 1
|
||||
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments = (qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener)
|
||||
DispatchQueue.main.async {
|
||||
self.buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations.append((qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener))
|
||||
}
|
||||
if let buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure {
|
||||
return try await buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure(qrCodeData, oidcConfiguration, progressListener)
|
||||
} else {
|
||||
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class BugReportServiceMock: BugReportServiceProtocol {
|
||||
var crashedLastRun: Bool {
|
||||
get { return underlyingCrashedLastRun }
|
||||
@@ -15296,6 +15520,271 @@ class UserSessionMock: UserSessionProtocol {
|
||||
var underlyingCallbacks: PassthroughSubject<UserSessionCallback, Never>!
|
||||
|
||||
}
|
||||
class UserSessionStoreMock: UserSessionStoreProtocol {
|
||||
var hasSessions: Bool {
|
||||
get { return underlyingHasSessions }
|
||||
set(value) { underlyingHasSessions = value }
|
||||
}
|
||||
var underlyingHasSessions: Bool!
|
||||
var userIDs: [String] = []
|
||||
var clientSessionDelegate: ClientSessionDelegate {
|
||||
get { return underlyingClientSessionDelegate }
|
||||
set(value) { underlyingClientSessionDelegate = value }
|
||||
}
|
||||
var underlyingClientSessionDelegate: ClientSessionDelegate!
|
||||
|
||||
//MARK: - reset
|
||||
|
||||
var resetUnderlyingCallsCount = 0
|
||||
var resetCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return resetUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = resetUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
resetUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
resetUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var resetCalled: Bool {
|
||||
return resetCallsCount > 0
|
||||
}
|
||||
var resetClosure: (() -> Void)?
|
||||
|
||||
func reset() {
|
||||
resetCallsCount += 1
|
||||
resetClosure?()
|
||||
}
|
||||
//MARK: - restoreUserSession
|
||||
|
||||
var restoreUserSessionUnderlyingCallsCount = 0
|
||||
var restoreUserSessionCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return restoreUserSessionUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = restoreUserSessionUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
restoreUserSessionUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
restoreUserSessionUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var restoreUserSessionCalled: Bool {
|
||||
return restoreUserSessionCallsCount > 0
|
||||
}
|
||||
|
||||
var restoreUserSessionUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionStoreError>!
|
||||
var restoreUserSessionReturnValue: Result<UserSessionProtocol, UserSessionStoreError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return restoreUserSessionUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<UserSessionProtocol, UserSessionStoreError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = restoreUserSessionUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
restoreUserSessionUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
restoreUserSessionUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var restoreUserSessionClosure: (() async -> Result<UserSessionProtocol, UserSessionStoreError>)?
|
||||
|
||||
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
restoreUserSessionCallsCount += 1
|
||||
if let restoreUserSessionClosure = restoreUserSessionClosure {
|
||||
return await restoreUserSessionClosure()
|
||||
} else {
|
||||
return restoreUserSessionReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - userSession
|
||||
|
||||
var userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = 0
|
||||
var userSessionForSessionDirectoriesPassphraseCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var userSessionForSessionDirectoriesPassphraseCalled: Bool {
|
||||
return userSessionForSessionDirectoriesPassphraseCallsCount > 0
|
||||
}
|
||||
var userSessionForSessionDirectoriesPassphraseReceivedArguments: (client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)?
|
||||
var userSessionForSessionDirectoriesPassphraseReceivedInvocations: [(client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)] = []
|
||||
|
||||
var userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionStoreError>!
|
||||
var userSessionForSessionDirectoriesPassphraseReturnValue: Result<UserSessionProtocol, UserSessionStoreError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<UserSessionProtocol, UserSessionStoreError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var userSessionForSessionDirectoriesPassphraseClosure: ((ClientProtocol, SessionDirectories, String?) async -> Result<UserSessionProtocol, UserSessionStoreError>)?
|
||||
|
||||
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
userSessionForSessionDirectoriesPassphraseCallsCount += 1
|
||||
userSessionForSessionDirectoriesPassphraseReceivedArguments = (client: client, sessionDirectories: sessionDirectories, passphrase: passphrase)
|
||||
DispatchQueue.main.async {
|
||||
self.userSessionForSessionDirectoriesPassphraseReceivedInvocations.append((client: client, sessionDirectories: sessionDirectories, passphrase: passphrase))
|
||||
}
|
||||
if let userSessionForSessionDirectoriesPassphraseClosure = userSessionForSessionDirectoriesPassphraseClosure {
|
||||
return await userSessionForSessionDirectoriesPassphraseClosure(client, sessionDirectories, passphrase)
|
||||
} else {
|
||||
return userSessionForSessionDirectoriesPassphraseReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - logout
|
||||
|
||||
var logoutUserSessionUnderlyingCallsCount = 0
|
||||
var logoutUserSessionCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return logoutUserSessionUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = logoutUserSessionUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
logoutUserSessionUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
logoutUserSessionUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var logoutUserSessionCalled: Bool {
|
||||
return logoutUserSessionCallsCount > 0
|
||||
}
|
||||
var logoutUserSessionReceivedUserSession: UserSessionProtocol?
|
||||
var logoutUserSessionReceivedInvocations: [UserSessionProtocol] = []
|
||||
var logoutUserSessionClosure: ((UserSessionProtocol) -> Void)?
|
||||
|
||||
func logout(userSession: UserSessionProtocol) {
|
||||
logoutUserSessionCallsCount += 1
|
||||
logoutUserSessionReceivedUserSession = userSession
|
||||
DispatchQueue.main.async {
|
||||
self.logoutUserSessionReceivedInvocations.append(userSession)
|
||||
}
|
||||
logoutUserSessionClosure?(userSession)
|
||||
}
|
||||
//MARK: - clearCache
|
||||
|
||||
var clearCacheForUnderlyingCallsCount = 0
|
||||
var clearCacheForCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return clearCacheForUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = clearCacheForUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
clearCacheForUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
clearCacheForUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var clearCacheForCalled: Bool {
|
||||
return clearCacheForCallsCount > 0
|
||||
}
|
||||
var clearCacheForReceivedUserID: String?
|
||||
var clearCacheForReceivedInvocations: [String] = []
|
||||
var clearCacheForClosure: ((String) -> Void)?
|
||||
|
||||
func clearCache(for userID: String) {
|
||||
clearCacheForCallsCount += 1
|
||||
clearCacheForReceivedUserID = userID
|
||||
DispatchQueue.main.async {
|
||||
self.clearCacheForReceivedInvocations.append(userID)
|
||||
}
|
||||
clearCacheForClosure?(userID)
|
||||
}
|
||||
}
|
||||
class VoiceMessageCacheMock: VoiceMessageCacheProtocol {
|
||||
var urlForRecording: URL {
|
||||
get { return underlyingUrlForRecording }
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension ClientSDKMock {
|
||||
struct Configuration {
|
||||
// MARK: Authentication
|
||||
|
||||
var serverAddress = "matrix.org"
|
||||
var homeserverURL = "https://matrix-client.matrix.org"
|
||||
var slidingSyncVersion = SlidingSyncVersion.native
|
||||
var oidcLoginURL: String?
|
||||
var supportsPasswordLogin = true
|
||||
var elementWellKnown = "{\"registration_helper_url\":\"https://develop.element.io/#/mobile_register\"}"
|
||||
var validCredentials = (username: "alice", password: "12345678")
|
||||
|
||||
// MARK: Session
|
||||
|
||||
var userID: String?
|
||||
var session = Session(accessToken: UUID().uuidString,
|
||||
refreshToken: nil,
|
||||
userId: "@alice:matrix.org",
|
||||
deviceId: UUID().uuidString,
|
||||
homeserverUrl: "https://matrix-client.matrix.org",
|
||||
oidcData: nil,
|
||||
slidingSyncVersion: .native)
|
||||
}
|
||||
|
||||
enum MockError: Error { case generic }
|
||||
|
||||
convenience init(configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
homeserverLoginDetailsReturnValue = HomeserverLoginDetailsSDKMock(configuration: configuration)
|
||||
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
|
||||
userIdServerNameThrowableError = MockError.generic
|
||||
serverReturnValue = "https://\(configuration.serverAddress)"
|
||||
getUrlUrlReturnValue = configuration.elementWellKnown
|
||||
urlForOidcLoginOidcConfigurationReturnValue = OidcAuthorizationDataSDKMock(configuration: configuration)
|
||||
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in
|
||||
guard username == configuration.validCredentials.username,
|
||||
password == configuration.validCredentials.password else {
|
||||
throw MockError.generic // use the matrix error
|
||||
}
|
||||
}
|
||||
|
||||
userIdReturnValue = configuration.userID
|
||||
sessionReturnValue = configuration.session
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeserverLoginDetailsSDKMock {
|
||||
convenience init(configuration: ClientSDKMock.Configuration) {
|
||||
self.init()
|
||||
|
||||
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
|
||||
supportsPasswordLoginReturnValue = configuration.supportsPasswordLogin
|
||||
supportsOidcLoginReturnValue = configuration.oidcLoginURL != nil
|
||||
urlReturnValue = configuration.homeserverURL
|
||||
}
|
||||
}
|
||||
|
||||
extension OidcAuthorizationDataSDKMock {
|
||||
convenience init(configuration: ClientSDKMock.Configuration) {
|
||||
self.init()
|
||||
|
||||
loginUrlReturnValue = configuration.oidcLoginURL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
extension UserSessionStoreMock {
|
||||
struct Configuration { }
|
||||
|
||||
convenience init(configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
userSessionForSessionDirectoriesPassphraseReturnValue = .success(UserSessionMock(.init(clientProxy: ClientProxyMock(.init()))))
|
||||
clientSessionDelegate = KeychainControllerMock()
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,11 @@ struct LoginHomeserver: Equatable {
|
||||
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
|
||||
/// - Trim any whitespace.
|
||||
/// - Lowercase the address.
|
||||
|
||||
@@ -145,7 +145,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
startLoading(isInteractionBlocking: false)
|
||||
|
||||
Task {
|
||||
switch await authenticationService.configure(for: homeserverDomain) {
|
||||
switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
|
||||
case .success:
|
||||
stopLoading()
|
||||
if authenticationService.homeserver.value.loginMode == .oidc {
|
||||
|
||||
+5
-1
@@ -11,6 +11,8 @@ import SwiftUI
|
||||
struct ServerConfirmationScreenCoordinatorParameters {
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
let authenticationFlow: AuthenticationFlow
|
||||
let slidingSyncLearnMoreURL: URL
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum ServerConfirmationScreenCoordinatorAction {
|
||||
@@ -29,7 +31,9 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: ServerConfirmationScreenCoordinatorParameters) {
|
||||
viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService,
|
||||
authenticationFlow: parameters.authenticationFlow)
|
||||
authenticationFlow: parameters.authenticationFlow,
|
||||
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
+21
-15
@@ -19,11 +19,11 @@ 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?
|
||||
|
||||
var bindings = ServerConfirmationScreenBindings()
|
||||
|
||||
/// The screen's title.
|
||||
var title: String {
|
||||
switch authenticationFlow {
|
||||
@@ -46,23 +46,16 @@ struct ServerConfirmationScreenViewState: BindableState {
|
||||
""
|
||||
}
|
||||
case .register:
|
||||
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
|
||||
L10n.screenServerConfirmationMessageRegister
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerConfirmationScreenBindings {
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<ServerConfirmationScreenAlert>?
|
||||
}
|
||||
|
||||
enum ServerConfirmationScreenViewAction {
|
||||
/// Updates the window used as the OIDC presentation anchor.
|
||||
case updateWindow(UIWindow)
|
||||
@@ -71,3 +64,16 @@ enum ServerConfirmationScreenViewAction {
|
||||
/// The user would like to change to a different homeserver.
|
||||
case changeServer
|
||||
}
|
||||
|
||||
enum ServerConfirmationScreenAlert: Hashable {
|
||||
/// An alert that informs the user that a server could not be found.
|
||||
case homeserverNotFound
|
||||
/// An alert that informs the user about a bad well-known file.
|
||||
case invalidWellKnown(String)
|
||||
/// An alert that allows the user to learn about sliding sync.
|
||||
case slidingSync
|
||||
/// An alert that informs the user that registration isn't supported.
|
||||
case registration
|
||||
/// An unknown error has occurred.
|
||||
case unknownError
|
||||
}
|
||||
|
||||
+85
-13
@@ -11,46 +11,118 @@ import SwiftUI
|
||||
typealias ServerConfirmationScreenViewModelType = StateStoreViewModel<ServerConfirmationScreenViewState, ServerConfirmationScreenViewAction>
|
||||
|
||||
class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol {
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
let authenticationFlow: AuthenticationFlow
|
||||
let slidingSyncLearnMoreURL: URL
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow) {
|
||||
let homeserver = authenticationService.homeserver.value
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
authenticationFlow: AuthenticationFlow,
|
||||
slidingSyncLearnMoreURL: URL,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.authenticationFlow = authenticationFlow
|
||||
self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
let homeserver = authenticationService.homeserver.value
|
||||
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address,
|
||||
authenticationFlow: authenticationFlow,
|
||||
homeserverSupportsRegistration: homeserver.supportsRegistration))
|
||||
authenticationFlow: authenticationFlow))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ServerConfirmationScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .updateWindow(let window):
|
||||
guard state.window != window else { return }
|
||||
Task { state.window = window }
|
||||
case .confirm:
|
||||
actionsSubject.send(.confirm)
|
||||
Task { await configureAndContinue() }
|
||||
case .changeServer:
|
||||
actionsSubject.send(.changeServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LoginHomeserver {
|
||||
var supportsRegistration: Bool {
|
||||
loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil)
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func configureAndContinue() async {
|
||||
let homeserver = authenticationService.homeserver.value
|
||||
|
||||
// If the login mode is unknown, the service hasn't be configured and we need to do it now.
|
||||
// Otherwise we can continue the flow as server selection has been performed and succeeded.
|
||||
guard homeserver.loginMode == .unknown || authenticationService.flow != authenticationFlow else {
|
||||
actionsSubject.send(.confirm)
|
||||
return
|
||||
}
|
||||
|
||||
startLoading()
|
||||
defer { stopLoading() }
|
||||
|
||||
switch await authenticationService.configure(for: homeserver.address, flow: authenticationFlow) {
|
||||
case .success:
|
||||
actionsSubject.send(.confirm)
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case .invalidServer, .invalidHomeserverAddress:
|
||||
displayError(.homeserverNotFound)
|
||||
case .invalidWellKnown(let error):
|
||||
displayError(.invalidWellKnown(error))
|
||||
case .slidingSyncNotAvailable:
|
||||
displayError(.slidingSync)
|
||||
case .registrationNotSupported:
|
||||
displayError(.registration)
|
||||
default:
|
||||
displayError(.unknownError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startLoading(label: String = L10n.commonLoading) {
|
||||
userIndicatorController.submitIndicator(UserIndicator(type: .modal,
|
||||
title: label,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func stopLoading() {
|
||||
userIndicatorController.retractAllIndicators()
|
||||
}
|
||||
|
||||
private func displayError(_ type: ServerConfirmationScreenAlert) {
|
||||
switch type {
|
||||
case .homeserverNotFound:
|
||||
state.bindings.alertInfo = AlertInfo(id: .homeserverNotFound,
|
||||
title: L10n.errorUnknown,
|
||||
message: L10n.screenChangeServerErrorInvalidHomeserver)
|
||||
case .invalidWellKnown(let error):
|
||||
state.bindings.alertInfo = AlertInfo(id: .invalidWellKnown(error),
|
||||
title: L10n.commonServerNotSupported,
|
||||
message: L10n.screenChangeServerErrorInvalidWellKnown(error))
|
||||
case .slidingSync:
|
||||
let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) }
|
||||
state.bindings.alertInfo = AlertInfo(id: .slidingSync,
|
||||
title: L10n.commonServerNotSupported,
|
||||
message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
|
||||
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
|
||||
secondaryButton: .init(title: L10n.actionCancel, action: nil))
|
||||
case .registration:
|
||||
state.bindings.alertInfo = AlertInfo(id: .registration,
|
||||
title: L10n.errorUnknown,
|
||||
message: L10n.errorAccountCreationNotPossible)
|
||||
case .unknownError:
|
||||
state.bindings.alertInfo = AlertInfo(id: .unknownError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -19,6 +19,7 @@ struct ServerConfirmationScreen: View {
|
||||
}
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgCanvasDefault)
|
||||
.alert(item: $context.alertInfo)
|
||||
.introspect(.window, on: .supportedVersions) { window in
|
||||
context.send(viewAction: .updateWindow(window))
|
||||
}
|
||||
@@ -53,7 +54,6 @@ struct ServerConfirmationScreen: View {
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue)
|
||||
.disabled(!context.viewState.canContinue)
|
||||
|
||||
Button { context.send(viewAction: .changeServer) } label: {
|
||||
Text(L10n.screenServerConfirmationChangeServer)
|
||||
@@ -68,10 +68,8 @@ struct ServerConfirmationScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(),
|
||||
authenticationFlow: .login)
|
||||
static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(),
|
||||
authenticationFlow: .register)
|
||||
static let loginViewModel = makeViewModel(flow: .login)
|
||||
static let registerViewModel = makeViewModel(flow: .register)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
@@ -86,4 +84,11 @@ struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
||||
}
|
||||
.previewDisplayName("Register")
|
||||
}
|
||||
|
||||
static func makeViewModel(flow: AuthenticationFlow) -> ServerConfirmationScreenViewModel {
|
||||
ServerConfirmationScreenViewModel(authenticationService: AuthenticationService.mock,
|
||||
authenticationFlow: flow,
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
+3
-11
@@ -11,30 +11,22 @@ enum MockServerSelectionScreenState: CaseIterable {
|
||||
case matrix
|
||||
case emptyAddress
|
||||
case invalidAddress
|
||||
case nonModal
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
@MainActor var viewModel: ServerSelectionScreenViewModel {
|
||||
switch self {
|
||||
case .matrix:
|
||||
return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org",
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
isModallyPresented: true)
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
|
||||
|
||||
case .emptyAddress:
|
||||
return ServerSelectionScreenViewModel(homeserverAddress: "",
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
isModallyPresented: true)
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
|
||||
case .invalidAddress:
|
||||
let viewModel = ServerSelectionScreenViewModel(homeserverAddress: "thisisbad",
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
isModallyPresented: true)
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
|
||||
viewModel.displayError(.footerMessage(L10n.errorUnknown))
|
||||
return viewModel
|
||||
case .nonModal:
|
||||
return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org",
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
isModallyPresented: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -11,9 +11,9 @@ import SwiftUI
|
||||
struct ServerSelectionScreenCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
let authenticationFlow: AuthenticationFlow
|
||||
let slidingSyncLearnMoreURL: URL
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
/// Whether the screen is presented modally or within a navigation stack.
|
||||
let isModallyPresented: Bool
|
||||
}
|
||||
|
||||
enum ServerSelectionScreenCoordinatorAction {
|
||||
@@ -38,8 +38,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: ServerSelectionScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address,
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
isModallyPresented: parameters.isModallyPresented)
|
||||
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL)
|
||||
userIndicatorController = parameters.userIndicatorController
|
||||
}
|
||||
|
||||
@@ -85,7 +84,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
|
||||
startLoading()
|
||||
|
||||
Task {
|
||||
switch await authenticationService.configure(for: homeserverAddress) {
|
||||
switch await authenticationService.configure(for: homeserverAddress, flow: parameters.authenticationFlow) {
|
||||
case .success:
|
||||
MXLog.info("Selected homeserver: \(homeserverAddress)")
|
||||
actionsSubject.send(.updated)
|
||||
@@ -107,6 +106,8 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel.displayError(.invalidWellKnownAlert(error))
|
||||
case .slidingSyncNotAvailable:
|
||||
viewModel.displayError(.slidingSyncAlert)
|
||||
case .registrationNotSupported:
|
||||
viewModel.displayError(.registrationAlert) // TODO: [DOUG] Test me!
|
||||
default:
|
||||
viewModel.displayError(.footerMessage(L10n.errorUnknown))
|
||||
}
|
||||
|
||||
+3
-9
@@ -22,19 +22,12 @@ struct ServerSelectionScreenViewState: BindableState {
|
||||
var bindings: ServerSelectionScreenBindings
|
||||
/// An error message to be shown in the text field footer.
|
||||
var footerErrorMessage: String?
|
||||
/// Whether the screen is presented modally or within a navigation stack.
|
||||
var isModallyPresented: Bool
|
||||
|
||||
/// The message to show in the text field footer.
|
||||
var footerMessage: AttributedString {
|
||||
footerErrorMessage.map(AttributedString.init) ?? regularFooterMessage
|
||||
}
|
||||
|
||||
/// The title shown on the confirm button.
|
||||
var buttonTitle: String {
|
||||
isModallyPresented ? L10n.actionContinue : L10n.actionNext
|
||||
}
|
||||
|
||||
/// The text field is showing an error.
|
||||
var isShowingFooterError: Bool {
|
||||
footerErrorMessage != nil
|
||||
@@ -45,10 +38,9 @@ struct ServerSelectionScreenViewState: BindableState {
|
||||
bindings.homeserverAddress.isEmpty || isShowingFooterError
|
||||
}
|
||||
|
||||
init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil, isModallyPresented: Bool) {
|
||||
init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil) {
|
||||
self.bindings = bindings
|
||||
self.footerErrorMessage = footerErrorMessage
|
||||
self.isModallyPresented = isModallyPresented
|
||||
|
||||
let linkPlaceholder = "{link}"
|
||||
var message = AttributedString(L10n.screenChangeServerFormNotice(linkPlaceholder))
|
||||
@@ -82,4 +74,6 @@ enum ServerSelectionScreenErrorType: Hashable {
|
||||
case invalidWellKnownAlert(String)
|
||||
/// An alert that allows the user to learn about sliding sync.
|
||||
case slidingSyncAlert
|
||||
/// An alert that informs the user that registration isn't supported.
|
||||
case registrationAlert
|
||||
}
|
||||
|
||||
+7
-4
@@ -19,13 +19,12 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(homeserverAddress: String, slidingSyncLearnMoreURL: URL, isModallyPresented: Bool) {
|
||||
init(homeserverAddress: String, slidingSyncLearnMoreURL: URL) {
|
||||
self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
|
||||
let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress)
|
||||
|
||||
super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL,
|
||||
bindings: bindings,
|
||||
isModallyPresented: isModallyPresented))
|
||||
bindings: bindings))
|
||||
}
|
||||
|
||||
override func process(viewAction: ServerSelectionScreenViewAction) {
|
||||
@@ -46,7 +45,7 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
|
||||
state.footerErrorMessage = message
|
||||
}
|
||||
case .invalidWellKnownAlert(let error):
|
||||
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
|
||||
state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error),
|
||||
title: L10n.commonServerNotSupported,
|
||||
message: L10n.screenChangeServerErrorInvalidWellKnown(error))
|
||||
case .slidingSyncAlert:
|
||||
@@ -56,6 +55,10 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
|
||||
message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
|
||||
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
|
||||
secondaryButton: .init(title: L10n.actionCancel, action: nil))
|
||||
case .registrationAlert:
|
||||
state.bindings.alertInfo = AlertInfo(id: .registrationAlert,
|
||||
title: L10n.errorUnknown,
|
||||
message: L10n.errorAccountCreationNotPossible)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-7
@@ -64,7 +64,7 @@ struct ServerSelectionScreen: View {
|
||||
.onSubmit(submit)
|
||||
|
||||
Button(action: submit) {
|
||||
Text(context.viewState.buttonTitle)
|
||||
Text(L10n.actionContinue)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.disabled(context.viewState.hasValidationError)
|
||||
@@ -72,15 +72,12 @@ struct ServerSelectionScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
if context.viewState.isModallyPresented {
|
||||
Button { context.send(viewAction: .dismiss) } label: {
|
||||
Text(L10n.actionCancel)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss)
|
||||
Button { context.send(viewAction: .dismiss) } label: {
|
||||
Text(L10n.actionCancel)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,16 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol AuthenticationClientBuilderProtocol {
|
||||
func build(homeserverAddress: String) async throws -> ClientProtocol
|
||||
func buildWithQRCode(qrCodeData: QrCodeData,
|
||||
oidcConfiguration: OIDCConfigurationProxy,
|
||||
progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol
|
||||
}
|
||||
|
||||
/// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins.
|
||||
struct AuthenticationClientBuilder {
|
||||
struct AuthenticationClientBuilder: AuthenticationClientBuilderProtocol {
|
||||
let sessionDirectories: SessionDirectories
|
||||
let passphrase: String
|
||||
let clientSessionDelegate: ClientSessionDelegate
|
||||
@@ -18,7 +26,7 @@ struct AuthenticationClientBuilder {
|
||||
let appHooks: AppHooks
|
||||
|
||||
/// Builds a Client for login using OIDC or password authentication.
|
||||
func build(homeserverAddress: String) async throws -> Client {
|
||||
func build(homeserverAddress: String) async throws -> ClientProtocol {
|
||||
if appSettings.slidingSyncDiscovery == .forceNative {
|
||||
return try await makeClientBuilder(slidingSync: .forceNative).serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress).build()
|
||||
}
|
||||
@@ -38,7 +46,7 @@ struct AuthenticationClientBuilder {
|
||||
/// Builds a Client, authenticating with the given QR code data.
|
||||
func buildWithQRCode(qrCodeData: QrCodeData,
|
||||
oidcConfiguration: OIDCConfigurationProxy,
|
||||
progressListener: QrLoginProgressListenerProxy) async throws -> Client {
|
||||
progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol {
|
||||
if appSettings.slidingSyncDiscovery == .forceNative {
|
||||
return try await makeClientBuilder(slidingSync: .forceNative).buildWithQrCode(qrCodeData: qrCodeData,
|
||||
oidcConfiguration: oidcConfiguration.rustValue,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol AuthenticationClientBuilderFactoryProtocol {
|
||||
func makeBuilder(sessionDirectories: SessionDirectories,
|
||||
passphrase: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) -> AuthenticationClientBuilderProtocol
|
||||
}
|
||||
|
||||
/// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins.
|
||||
struct AuthenticationClientBuilderFactory: AuthenticationClientBuilderFactoryProtocol {
|
||||
func makeBuilder(sessionDirectories: SessionDirectories,
|
||||
passphrase: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) -> AuthenticationClientBuilderProtocol {
|
||||
AuthenticationClientBuilder(sessionDirectories: sessionDirectories,
|
||||
passphrase: passphrase,
|
||||
clientSessionDelegate: clientSessionDelegate,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
}
|
||||
}
|
||||
@@ -10,31 +10,39 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class AuthenticationService: AuthenticationServiceProtocol {
|
||||
private var client: Client?
|
||||
private var client: ClientProtocol?
|
||||
private var sessionDirectories: SessionDirectories
|
||||
private let passphrase: String
|
||||
|
||||
private let clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let appHooks: AppHooks
|
||||
|
||||
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
|
||||
private(set) var flow: AuthenticationFlow
|
||||
|
||||
init(userSessionStore: UserSessionStoreProtocol, encryptionKeyProvider: EncryptionKeyProviderProtocol, appSettings: AppSettings, appHooks: AppHooks) {
|
||||
init(userSessionStore: UserSessionStoreProtocol,
|
||||
encryptionKeyProvider: EncryptionKeyProviderProtocol,
|
||||
clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol = AuthenticationClientBuilderFactory(),
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) {
|
||||
sessionDirectories = .init()
|
||||
passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
|
||||
self.clientBuilderFactory = clientBuilderFactory
|
||||
self.userSessionStore = userSessionStore
|
||||
self.appSettings = appSettings
|
||||
self.appHooks = appHooks
|
||||
|
||||
homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress,
|
||||
loginMode: .unknown))
|
||||
// When updating these, don't forget to update the reset method too.
|
||||
homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown))
|
||||
flow = .login
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError> {
|
||||
do {
|
||||
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
|
||||
|
||||
@@ -57,7 +65,12 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
case .failure: nil
|
||||
}
|
||||
|
||||
if flow == .register, !homeserver.supportsRegistration {
|
||||
return .failure(.registrationNotSupported)
|
||||
}
|
||||
|
||||
self.client = client
|
||||
self.flow = flow
|
||||
homeserverSubject.send(homeserver)
|
||||
return .success(())
|
||||
} catch ClientBuildError.WellKnownDeserializationError(let error) {
|
||||
@@ -150,18 +163,24 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
homeserverSubject.send(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown))
|
||||
flow = .login
|
||||
client = nil
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func makeClientBuilder() -> AuthenticationClientBuilder {
|
||||
private func makeClientBuilder() -> AuthenticationClientBuilderProtocol {
|
||||
// Use a fresh session directory each time the user enters a different server
|
||||
// so that caches (e.g. server versions) are always fresh for the new server.
|
||||
rotateSessionDirectory()
|
||||
|
||||
return AuthenticationClientBuilder(sessionDirectories: sessionDirectories,
|
||||
passphrase: passphrase,
|
||||
clientSessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
return clientBuilderFactory.makeBuilder(sessionDirectories: sessionDirectories,
|
||||
passphrase: passphrase,
|
||||
clientSessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
}
|
||||
|
||||
private func rotateSessionDirectory() {
|
||||
@@ -169,7 +188,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
sessionDirectories = .init()
|
||||
}
|
||||
|
||||
private func userSession(for client: Client) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
private func userSession(for client: ClientProtocol) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
|
||||
case .success(let clientProxy):
|
||||
return .success(clientProxy)
|
||||
@@ -178,3 +197,13 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mocks
|
||||
|
||||
extension AuthenticationService {
|
||||
static var mock = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()),
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ enum AuthenticationFlow {
|
||||
case register
|
||||
}
|
||||
|
||||
enum AuthenticationServiceError: Error {
|
||||
enum AuthenticationServiceError: Error, Equatable {
|
||||
/// An error occurred during OIDC authentication.
|
||||
case oidcError(OIDCError)
|
||||
case invalidServer
|
||||
@@ -24,6 +24,7 @@ enum AuthenticationServiceError: Error {
|
||||
case invalidHomeserverAddress
|
||||
case invalidWellKnown(String)
|
||||
case slidingSyncNotAvailable
|
||||
case registrationNotSupported
|
||||
case accountDeactivated
|
||||
case failedLoggingIn
|
||||
case sessionTokenRefreshNotSupported
|
||||
@@ -33,9 +34,11 @@ enum AuthenticationServiceError: Error {
|
||||
protocol AuthenticationServiceProtocol {
|
||||
/// The currently configured homeserver.
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
|
||||
/// The type of flow the service is currently configured with.
|
||||
var flow: AuthenticationFlow { get }
|
||||
|
||||
/// Sets up the service for login on the specified homeserver address.
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
|
||||
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
|
||||
/// Performs login using OIDC for the current homeserver.
|
||||
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError>
|
||||
/// Asks the SDK to abort an ongoing OIDC login if we didn't get a callback to complete the request with.
|
||||
@@ -46,6 +49,9 @@ protocol AuthenticationServiceProtocol {
|
||||
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()
|
||||
}
|
||||
|
||||
// MARK: - OIDC
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class MockAuthenticationService: AuthenticationServiceProtocol {
|
||||
let validCredentials = (username: "alice", password: "12345678")
|
||||
|
||||
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
|
||||
|
||||
init(homeserver: LoginHomeserver = .mockMatrixDotOrg) {
|
||||
homeserverSubject = .init(homeserver)
|
||||
}
|
||||
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
// Map the address to the mock homeservers
|
||||
if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) {
|
||||
homeserverSubject.send(.mockMatrixDotOrg)
|
||||
return .success(())
|
||||
} else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) {
|
||||
homeserverSubject.send(.mockOIDC)
|
||||
return .success(())
|
||||
} else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) {
|
||||
homeserverSubject.send(.mockBasicServer)
|
||||
return .success(())
|
||||
} else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) {
|
||||
homeserverSubject.send(.mockUnsupported)
|
||||
return .success(())
|
||||
} else {
|
||||
// Otherwise fail with an invalid server.
|
||||
return .failure(.invalidServer)
|
||||
}
|
||||
}
|
||||
|
||||
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> {
|
||||
.failure(.oidcError(.notSupported))
|
||||
}
|
||||
|
||||
func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async { }
|
||||
|
||||
func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
.failure(.oidcError(.notSupported))
|
||||
}
|
||||
|
||||
func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
// Login only succeeds if the username and password match the valid credentials property
|
||||
guard username == validCredentials.username, password == validCredentials.password else {
|
||||
return .failure(.invalidCredentials)
|
||||
}
|
||||
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: username))))
|
||||
return .success(userSession)
|
||||
}
|
||||
|
||||
func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result<any UserSessionProtocol, AuthenticationServiceError> {
|
||||
.failure(.failedLoggingIn)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol {
|
||||
sessionDirectories = .init()
|
||||
}
|
||||
|
||||
private func userSession(for client: Client) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
|
||||
private func userSession(for client: ClientProtocol) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
|
||||
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
|
||||
case .success(let session):
|
||||
return .success(session)
|
||||
|
||||
@@ -60,7 +60,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
do {
|
||||
let session = try client.session()
|
||||
let userID = try client.userId()
|
||||
@@ -146,7 +146,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func setupProxyForClient(_ client: Client) async -> ClientProxyProtocol {
|
||||
private func setupProxyForClient(_ client: ClientProtocol) async -> ClientProxyProtocol {
|
||||
await ClientProxy(client: client,
|
||||
networkMonitor: networkMonitor,
|
||||
appSettings: appSettings)
|
||||
|
||||
@@ -14,6 +14,7 @@ enum UserSessionStoreError: Error {
|
||||
case failedSettingUpSession
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol UserSessionStoreProtocol {
|
||||
/// Deletes all data stored in the shared container and keychain
|
||||
func reset()
|
||||
@@ -31,7 +32,7 @@ protocol UserSessionStoreProtocol {
|
||||
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
/// Creates a user session for a new client from the SDK along with the passphrase used for the data stores.
|
||||
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
/// Logs out of the specified session.
|
||||
func logout(userSession: UserSessionProtocol)
|
||||
|
||||
@@ -109,22 +109,16 @@ class MockScreen: Identifiable {
|
||||
|
||||
lazy var coordinator: CoordinatorProtocol? = {
|
||||
switch id {
|
||||
case .login:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = LoginScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .serverSelection:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
isModallyPresented: true))
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock,
|
||||
authenticationFlow: .login,
|
||||
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .authenticationFlow:
|
||||
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: MockAuthenticationService(),
|
||||
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock,
|
||||
qrCodeLoginService: QRCodeLoginServiceMock(),
|
||||
bugReportService: BugReportServiceMock(),
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
|
||||
@@ -20,7 +20,6 @@ enum UITestsScreenIdentifier: String {
|
||||
case createPoll
|
||||
case createRoom
|
||||
case createRoomNoUsers
|
||||
case login
|
||||
case roomLayoutBottom
|
||||
case roomLayoutMiddle
|
||||
case roomLayoutTop
|
||||
|
||||
Reference in New Issue
Block a user