// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 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 Foundation import MatrixRustSDK /// Represents a particular authentication flow. enum AuthenticationFlow { /// The flow for signing in to an existing account. case login /// The flow for creating a new account. case register } enum AuthenticationServiceError: Error, Equatable { /// An error occurred during OAuth authentication. case oAuthError(OAuthError) /// An error occurred during login with QR Code. case qrCodeError(QRCodeLoginError) case invalidServer case invalidCredentials case invalidHomeserverAddress case invalidWellKnown(String) case slidingSyncNotAvailable case loginNotSupported case registrationNotSupported case elementProRequired(serverName: String) case accountDeactivated case failedLoggingIn case sessionTokenRefreshNotSupported case failedUsingWebCredentials } protocol AuthenticationServiceProtocol: QRCodeLoginServiceProtocol { /// The currently configured homeserver. var homeserver: CurrentValuePublisher { 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, flow: AuthenticationFlow) async -> Result /// Performs login using OAuth for the current homeserver. func urlForOAuthLogin(loginHint: String?) async -> Result /// Asks the SDK to abort an ongoing OAuth login if we didn't get a callback to complete the request with. func abortOAuthLogin(data: OAuthAuthorizationDataProxy) async /// Completes an OAuth login that was started using ``urlForOAuthLogin``. func loginWithOAuthCallback(_ callbackURL: URL) async -> Result /// Performs a password login using the current homeserver. func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result /// Resets the current configuration requiring `configure(for:flow:)` to be called again. func reset() // MARK: - Classic App /// Account details discovered from the Classic app that is used for automatic verification when the same account is authenticated. var classicAppAccount: ClassicAppAccount? { get } /// Populates the Classic app account's state by checking if the homeserver is supported and which secrets are available. /// /// **Note:** This is no longer automatic purely for testing purposes. It needs to have been called before using ``classicAppAccount``. func setupClassicAppAccountState() async /// This can be called whenever the user has potentially updated their secrets in the Classic app. func refreshClassicAppAccountState() async } // MARK: - OAuth enum OAuthError: Error { /// Failed to get the URL that should be presented for login. case urlFailure /// The user cancelled the login. case userCancellation /// OAuth isn't supported on the currently configured server. case notSupported /// An unknown error occurred. case unknown } struct OAuthAuthorizationDataProxy: Hashable { let underlyingData: OAuthAuthorizationData var url: URL { guard let url = URL(string: underlyingData.loginUrl()) else { fatalError("OAuth login URL hasn't been validated.") } return url } } extension OAuthAuthorizationData: @retroactive Hashable { public static func == (lhs: MatrixRustSDK.OAuthAuthorizationData, rhs: MatrixRustSDK.OAuthAuthorizationData) -> Bool { lhs.loginUrl() == rhs.loginUrl() } public func hash(into hasher: inout Hasher) { hasher.combine(loginUrl()) } } // MARK: - Login with QR code enum QRCodeLoginError: Error, Equatable { case invalidQRCode case providerNotAllowed(scannedProvider: String, allowedProviders: [String]) case cancelled case connectionInsecure case declined case linkingNotSupported case expired case slidingSyncNotAvailable case deviceNotSignedIn case deviceAlreadySignedIn case unknown } // sourcery: AutoMockable protocol QRCodeLoginServiceProtocol { typealias QRLoginProgressPublisher = CurrentValuePublisher func loginWithQRCode(data: Data) -> QRLoginProgressPublisher } enum QRLoginProgress { case starting case establishingSecureChannel(checkCode: UInt8, checkCodeString: String) case waitingForToken(userCode: String) case syncingSecrets case signedIn(UserSessionProtocol) init?(rustProgress: QrLoginProgress) { switch rustProgress { case .starting: self = .starting case .establishingSecureChannel(let checkCode, let checkCodeString): self = .establishingSecureChannel(checkCode: checkCode, checkCodeString: checkCodeString) case .waitingForToken(let userCode): self = .waitingForToken(userCode: userCode) case .syncingSecrets: self = .syncingSecrets case .done: return nil // The SDK is done, but the app still needs to set up the UserSession. } } } extension QRLoginProgress: Equatable, CustomStringConvertible { static func == (lhs: QRLoginProgress, rhs: QRLoginProgress) -> Bool { switch (lhs, rhs) { case (.starting, .starting): true case let (.establishingSecureChannel(lhsCheckCode, lhsCheckCodeString), .establishingSecureChannel(rhsCheckCode, rhsCheckCodeString)): lhsCheckCode == rhsCheckCode && lhsCheckCodeString == rhsCheckCodeString case let (.waitingForToken(lhsUserCode), .waitingForToken(rhsUserCode)): lhsUserCode == rhsUserCode case (.syncingSecrets, .syncingSecrets): true case (.signedIn, .signedIn): true default: false } } var description: String { switch self { case .starting: "starting" case .establishingSecureChannel: "establishingSecureChannel" case .waitingForToken: "waitingForToken" case .syncingSecrets: "syncingSecrets" case .signedIn: "signedIn" } } }