mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Log out (#174)
* Expose logout method from SDK * Use logout from SDK, refactor logging out states * Add some strings * Introduce `initialDisplayName` on `UIDevice` * Implement soft logout screen * Add `softLogout` test screen identifier * Add new fields into the client proxy and implement new delegate methods * Add new fields into the user session and observe new client callbacks * Implement updated login method * Add remote logout state and event into the state machine * Implement refreshing restore token on `UserSessionStore` * Update app coordinator with new states and display soft logout screen when appropriate * Add reference screenshots for soft logout screen * Disable auto requesting photos access on screenshot detection * Make initial device name optional, generate project file * Add changelog * Use logout from SDK, refactor logging out states * Implement soft logout screen * Implement updated login method * Make initial device name optional, generate project file * Fix renamed event * Fix logout race * Remove redundant strings * Reuse existing strings * Confirm clear all data * Expose logout method from SDK * Use logout from SDK, refactor logging out states * Add some strings * Introduce `initialDisplayName` on `UIDevice` * Implement soft logout screen * Add `softLogout` test screen identifier * Add new fields into the client proxy and implement new delegate methods * Add new fields into the user session and observe new client callbacks * Implement updated login method * Add remote logout state and event into the state machine * Implement refreshing restore token on `UserSessionStore` * Update app coordinator with new states and display soft logout screen when appropriate * Add reference screenshots for soft logout screen * Disable auto requesting photos access on screenshot detection * Make initial device name optional, generate project file * Add changelog * Use logout from SDK, refactor logging out states * Implement soft logout screen * Implement updated login method * Make initial device name optional, generate project file * Fix renamed event * Fix logout race * Remove redundant strings * Reuse existing strings * Confirm clear all data * Comment out new apis for now * Make the PR compile
This commit is contained in:
@@ -31,7 +31,7 @@ struct ServiceLocator {
|
||||
let userIndicatorPresenter: UserIndicatorTypePresenter
|
||||
}
|
||||
|
||||
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
class AppCoordinator: Coordinator {
|
||||
private let window: UIWindow
|
||||
|
||||
private let stateMachine: AppCoordinatorStateMachine
|
||||
@@ -43,7 +43,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
|
||||
private var userSession: UserSessionProtocol!
|
||||
private var userSession: UserSessionProtocol! {
|
||||
didSet {
|
||||
deobserveUserSessionChanges()
|
||||
if let userSession = userSession, !userSession.isSoftLogout {
|
||||
observeUserSessionChanges()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let memberDetailProviderManager: MemberDetailProviderManager
|
||||
|
||||
@@ -53,6 +60,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var statusIndicator: UserIndicator?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
@@ -96,15 +105,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
window.makeKeyAndVisible()
|
||||
stateMachine.processEvent(userSessionStore.hasSessions ? .startWithExistingSession : .startWithAuthentication)
|
||||
}
|
||||
|
||||
// MARK: - AuthenticationCoordinatorDelegate
|
||||
|
||||
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
remove(childCoordinator: authenticationCoordinator)
|
||||
stateMachine.processEvent(.succeededSigningIn)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupLogging() {
|
||||
@@ -147,6 +148,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
case (.restoringSession, .failedRestoringSession, .signedOut):
|
||||
self.hideLoadingIndicator()
|
||||
self.showLoginErrorToast()
|
||||
self.startAuthentication()
|
||||
case (.restoringSession, .succeededRestoringSession, .homeScreen):
|
||||
self.hideLoadingIndicator()
|
||||
self.presentHomeScreen()
|
||||
@@ -155,13 +157,20 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
self.presentRoomWithIdentifier(roomId)
|
||||
case(.roomScreen, .dismissedRoomScreen, .homeScreen):
|
||||
self.tearDownDismissedRoomScreen()
|
||||
case (_, .attemptSignOut, .signingOut):
|
||||
self.userSessionStore.logout(userSession: self.userSession)
|
||||
self.stateMachine.processEvent(.succeededSigningOut)
|
||||
case (.signingOut, .succeededSigningOut, .signedOut):
|
||||
|
||||
case (_, .signOut, .signingOut):
|
||||
self.showLoadingIndicator()
|
||||
self.tearDownUserSession()
|
||||
case (.signingOut, .failedSigningOut, _):
|
||||
self.showLogoutErrorToast()
|
||||
case (.signingOut, .completedSigningOut, .signedOut):
|
||||
self.presentSplashScreen()
|
||||
self.hideLoadingIndicator()
|
||||
|
||||
case (_, .remoteSignOut(let isSoft), .remoteSigningOut):
|
||||
self.showLoadingIndicator()
|
||||
self.tearDownUserSession(isSoftLogout: isSoft)
|
||||
case (.remoteSigningOut(let isSoft), .completedSigningOut, .signedOut):
|
||||
self.presentSplashScreen(isSoftLogout: isSoft)
|
||||
self.hideLoadingIndicator()
|
||||
|
||||
case (.homeScreen, .showSettingsScreen, .settingsScreen):
|
||||
self.presentSettingsScreen()
|
||||
@@ -190,7 +199,11 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
switch await userSessionStore.restoreUserSession() {
|
||||
case .success(let userSession):
|
||||
self.userSession = userSession
|
||||
stateMachine.processEvent(.succeededRestoringSession)
|
||||
if userSession.isSoftLogout {
|
||||
stateMachine.processEvent(.remoteSignOut(isSoft: true))
|
||||
} else {
|
||||
stateMachine.processEvent(.succeededRestoringSession)
|
||||
}
|
||||
case .failure:
|
||||
MXLog.error("Failed to restore an existing session.")
|
||||
stateMachine.processEvent(.failedRestoringSession)
|
||||
@@ -207,17 +220,80 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func startAuthenticationSoftLogout() {
|
||||
Task {
|
||||
var displayName = ""
|
||||
if case .success(let name) = await userSession.clientProxy.loadUserDisplayName() {
|
||||
displayName = name
|
||||
}
|
||||
|
||||
let credentials = SoftLogoutCredentials(userId: userSession.userID,
|
||||
homeserverName: userSession.homeserver,
|
||||
userDisplayName: displayName,
|
||||
deviceId: userSession.deviceId)
|
||||
|
||||
let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore)
|
||||
_ = await authenticationService.configure(for: userSession.homeserver)
|
||||
|
||||
let parameters = SoftLogoutCoordinatorParameters(authenticationService: authenticationService,
|
||||
credentials: credentials,
|
||||
keyBackupNeeded: false)
|
||||
let coordinator = SoftLogoutCoordinator(parameters: parameters)
|
||||
coordinator.callback = { result in
|
||||
switch result {
|
||||
case .signedIn(let session):
|
||||
self.userSession = session
|
||||
self.remove(childCoordinator: coordinator)
|
||||
self.stateMachine.processEvent(.succeededSigningIn)
|
||||
case .clearAllData:
|
||||
self.confirmClearAllData {
|
||||
// clear user data
|
||||
self.userSessionStore.logout(userSession: self.userSession)
|
||||
self.userSession = nil
|
||||
self.remove(childCoordinator: coordinator)
|
||||
self.startAuthentication()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
|
||||
navigationRouter.setRootModule(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownUserSession() {
|
||||
private func tearDownUserSession(isSoftLogout: Bool = false) {
|
||||
Task {
|
||||
deobserveUserSessionChanges()
|
||||
|
||||
if !isSoftLogout {
|
||||
// first log out from the server
|
||||
_ = await userSession.clientProxy.logout()
|
||||
|
||||
// regardless of the result, clear user data
|
||||
userSessionStore.logout(userSession: userSession)
|
||||
userSession = nil
|
||||
}
|
||||
|
||||
// complete logging out
|
||||
stateMachine.processEvent(.completedSigningOut)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentSplashScreen(isSoftLogout: Bool = false) {
|
||||
if let presentedCoordinator = childCoordinators.first {
|
||||
remove(childCoordinator: presentedCoordinator)
|
||||
}
|
||||
|
||||
userSession = nil
|
||||
|
||||
|
||||
mainNavigationController.setViewControllers([splashViewController], animated: false)
|
||||
|
||||
startAuthentication()
|
||||
|
||||
if isSoftLogout {
|
||||
startAuthenticationSoftLogout()
|
||||
} else {
|
||||
startAuthentication()
|
||||
}
|
||||
}
|
||||
|
||||
private func presentHomeScreen() {
|
||||
@@ -250,6 +326,29 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
showCrashPopup()
|
||||
}
|
||||
}
|
||||
|
||||
private func observeUserSessionChanges() {
|
||||
userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
switch callback {
|
||||
case .didReceiveAuthError(let isSoftLogout):
|
||||
self.stateMachine.processEvent(.remoteSignOut(isSoft: isSoftLogout))
|
||||
case .updateRestoreTokenNeeded:
|
||||
if let userSession = self.userSession {
|
||||
_ = self.userSessionStore.refreshRestoreToken(for: userSession)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func deobserveUserSessionChanges() {
|
||||
cancellables.forEach { $0.cancel() }
|
||||
cancellables.removeAll()
|
||||
}
|
||||
|
||||
// MARK: Rooms
|
||||
|
||||
@@ -305,7 +404,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
guard let self = self else { return }
|
||||
switch action {
|
||||
case .logout:
|
||||
self.stateMachine.processEvent(.attemptSignOut)
|
||||
self.stateMachine.processEvent(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +445,20 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { [weak self] _ in
|
||||
self?.stateMachine.processEvent(.attemptSignOut)
|
||||
self?.stateMachine.processEvent(.signOut)
|
||||
})
|
||||
|
||||
navigationRouter.present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// Shows a confirmation to clear all data, and proceeds to do so if the user confirms.
|
||||
private func confirmClearAllData(_ confirmed: @escaping () -> Void) {
|
||||
let alert = UIAlertController(title: ElementL10n.softLogoutClearDataDialogTitle,
|
||||
message: ElementL10n.softLogoutClearDataDialogContent,
|
||||
preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { _ in
|
||||
confirmed()
|
||||
})
|
||||
|
||||
navigationRouter.present(alert, animated: true)
|
||||
@@ -444,8 +556,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
private func showLoginErrorToast() {
|
||||
statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging in"))
|
||||
}
|
||||
|
||||
private func showLogoutErrorToast() {
|
||||
statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging out"))
|
||||
}
|
||||
|
||||
// MARK: - AuthenticationCoordinatorDelegate
|
||||
|
||||
extension AppCoordinator: AuthenticationCoordinatorDelegate {
|
||||
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
remove(childCoordinator: authenticationCoordinator)
|
||||
stateMachine.processEvent(.succeededSigningIn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ class AppCoordinatorStateMachine {
|
||||
|
||||
/// Processing a sign out request
|
||||
case signingOut
|
||||
|
||||
/// Processing a remote sign out
|
||||
case remoteSigningOut(isSoft: Bool)
|
||||
}
|
||||
|
||||
/// Events that can be triggered on the AppCoordinator state machine
|
||||
@@ -58,11 +61,11 @@ class AppCoordinatorStateMachine {
|
||||
case failedRestoringSession
|
||||
|
||||
/// Request sign out
|
||||
case attemptSignOut
|
||||
/// Signing out succeeded
|
||||
case succeededSigningOut
|
||||
/// Signing out failed
|
||||
case failedSigningOut
|
||||
case signOut
|
||||
/// Remote sign out.
|
||||
case remoteSignOut(isSoft: Bool)
|
||||
/// Signing out completed
|
||||
case completedSigningOut
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomId:the room identifier
|
||||
@@ -92,10 +95,8 @@ class AppCoordinatorStateMachine {
|
||||
machine.addRoutes(event: .succeededRestoringSession, transitions: [.restoringSession => .homeScreen])
|
||||
machine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
|
||||
|
||||
machine.addRoutes(event: .attemptSignOut, transitions: [.any => .signingOut])
|
||||
|
||||
machine.addRoutes(event: .succeededSigningOut, transitions: [.signingOut => .signedOut])
|
||||
machine.addRoutes(event: .failedSigningOut, transitions: [.signingOut => .settingsScreen])
|
||||
machine.addRoutes(event: .signOut, transitions: [.any => .signingOut])
|
||||
machine.addRoutes(event: .completedSigningOut, transitions: [.signingOut => .signedOut])
|
||||
|
||||
machine.addRoutes(event: .showSettingsScreen, transitions: [.homeScreen => .settingsScreen])
|
||||
machine.addRoutes(event: .dismissedSettingsScreen, transitions: [.settingsScreen => .homeScreen])
|
||||
@@ -110,6 +111,10 @@ class AppCoordinatorStateMachine {
|
||||
return .roomScreen(roomId: roomId)
|
||||
case (.dismissedRoomScreen, .roomScreen):
|
||||
return .homeScreen
|
||||
case (.remoteSignOut(let isSoft), _):
|
||||
return .remoteSigningOut(isSoft: isSoft)
|
||||
case (.completedSigningOut, .remoteSigningOut):
|
||||
return .signedOut
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ extension ElementL10n {
|
||||
public static let authenticationServerInfoMatrixDescription = ElementL10n.tr("Untranslated", "authentication_server_info_matrix_description")
|
||||
/// Choose your server to store your data
|
||||
public static let authenticationServerInfoTitle = ElementL10n.tr("Untranslated", "authentication_server_info_title")
|
||||
/// Mobile
|
||||
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
||||
/// Tablet
|
||||
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
|
||||
/// Failed creating the permalink
|
||||
public static let roomTimelinePermalinkCreationFailure = ElementL10n.tr("Untranslated", "room_timeline_permalink_creation_failure")
|
||||
/// Replying to %@
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIDevice {
|
||||
/// Returns if the device is a Phone
|
||||
var isPhone: Bool {
|
||||
userInterfaceIdiom == .phone
|
||||
}
|
||||
|
||||
var initialDisplayName: String {
|
||||
let string = isPhone ? ElementL10n.loginMobileDevice : ElementL10n.loginTabletDevice
|
||||
return "X \(string)"
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,10 @@ final class LoginCoordinator: Coordinator, Presentable {
|
||||
startLoading(isInteractionBlocking: true)
|
||||
|
||||
Task {
|
||||
switch await authenticationService.login(username: username, password: password) {
|
||||
switch await authenticationService.login(username: username,
|
||||
password: password,
|
||||
initialDeviceName: UIDevice.current.initialDisplayName,
|
||||
deviceId: nil) {
|
||||
case .success(let userSession):
|
||||
callback?(.signedIn(userSession))
|
||||
stopLoading()
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Using an enum for the screen allows you define the different state cases with
|
||||
/// the relevant associated data for each case.
|
||||
enum MockSoftLogoutScreenState: String, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case emptyPassword
|
||||
case enteredPassword
|
||||
case oidc
|
||||
case unsupported
|
||||
case keyBackupNeeded
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
@MainActor var viewModel: SoftLogoutViewModel {
|
||||
let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org",
|
||||
homeserverName: "matrix.org",
|
||||
userDisplayName: "mock",
|
||||
deviceId: nil)
|
||||
switch self {
|
||||
case .emptyPassword:
|
||||
return SoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockMatrixDotOrg,
|
||||
keyBackupNeeded: false)
|
||||
case .enteredPassword:
|
||||
return SoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockMatrixDotOrg,
|
||||
keyBackupNeeded: false,
|
||||
password: "12345678")
|
||||
case .oidc:
|
||||
return SoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockOIDC,
|
||||
keyBackupNeeded: false)
|
||||
case .unsupported:
|
||||
return SoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockUnsupported,
|
||||
keyBackupNeeded: false)
|
||||
case .keyBackupNeeded:
|
||||
return SoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockMatrixDotOrg,
|
||||
keyBackupNeeded: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MockSoftLogoutScreenState: Identifiable {
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AppAuth
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct SoftLogoutCoordinatorParameters {
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
let credentials: SoftLogoutCredentials
|
||||
let keyBackupNeeded: Bool
|
||||
}
|
||||
|
||||
enum SoftLogoutCoordinatorResult: CustomStringConvertible {
|
||||
/// Login was successful.
|
||||
case signedIn(UserSessionProtocol)
|
||||
/// Clear all user data
|
||||
case clearAllData
|
||||
|
||||
/// A string representation of the result, ignoring any associated values that could leak PII.
|
||||
var description: String {
|
||||
switch self {
|
||||
case .signedIn:
|
||||
return "signedIn"
|
||||
case .clearAllData:
|
||||
return "clearAllData"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SoftLogoutCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: SoftLogoutCoordinatorParameters
|
||||
private let softLogoutHostingController: UIViewController
|
||||
private var softLogoutViewModel: SoftLogoutViewModelProtocol
|
||||
/// Passed to the OIDC service to provide a view controller from which to present the authentication session.
|
||||
private let oidcUserAgent: OIDExternalUserAgentIOS?
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var successIndicator: UserIndicator?
|
||||
|
||||
/// The wizard used to handle the registration flow.
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (SoftLogoutCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: SoftLogoutCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let homeserver = parameters.authenticationService.homeserver
|
||||
|
||||
let viewModel = SoftLogoutViewModel(credentials: parameters.credentials,
|
||||
homeserver: homeserver,
|
||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||
softLogoutViewModel = viewModel
|
||||
|
||||
let view = SoftLogoutScreen(context: viewModel.context)
|
||||
softLogoutHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: softLogoutHostingController)
|
||||
oidcUserAgent = OIDExternalUserAgentIOS(presenting: softLogoutHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[SoftLogoutCoordinator] did start.")
|
||||
|
||||
softLogoutViewModel.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(result).")
|
||||
|
||||
switch result {
|
||||
case .login(let password):
|
||||
self.login(withPassword: password)
|
||||
case .forgotPassword:
|
||||
self.showForgotPasswordScreen()
|
||||
case .clearAllData:
|
||||
self.callback?(.clearAllData)
|
||||
case .continueWithOIDC:
|
||||
self.loginWithOIDC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
softLogoutHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
@MainActor private func startLoading() {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
@MainActor private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
@MainActor private func showForgotPasswordScreen() {
|
||||
MXLog.debug("[SoftLogoutCoordinator] showForgotPasswordScreen")
|
||||
|
||||
softLogoutViewModel.displayError(.alert("Not implemented."))
|
||||
}
|
||||
|
||||
/// Login with the supplied username and password.
|
||||
@MainActor private func login(withPassword password: String) {
|
||||
let username = parameters.credentials.userId
|
||||
|
||||
startLoading()
|
||||
|
||||
Task {
|
||||
switch await authenticationService.login(username: username,
|
||||
password: password,
|
||||
initialDeviceName: UIDevice.current.initialDisplayName,
|
||||
deviceId: parameters.credentials.deviceId) {
|
||||
case .success(let userSession):
|
||||
callback?(.signedIn(userSession))
|
||||
stopLoading()
|
||||
case .failure(let error):
|
||||
stopLoading()
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loginWithOIDC() {
|
||||
guard let oidcUserAgent = oidcUserAgent else {
|
||||
handleError(AuthenticationServiceError.oidcError(.notSupported))
|
||||
return
|
||||
}
|
||||
|
||||
startLoading()
|
||||
|
||||
Task {
|
||||
switch await authenticationService.loginWithOIDC(userAgent: oidcUserAgent) {
|
||||
case .success(let userSession):
|
||||
callback?(.signedIn(userSession))
|
||||
stopLoading()
|
||||
case .failure(let error):
|
||||
stopLoading()
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
private func handleError(_ error: AuthenticationServiceError) {
|
||||
switch error {
|
||||
case .invalidCredentials:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
|
||||
case .accountDeactivated:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
|
||||
default:
|
||||
softLogoutViewModel.displayError(.alert(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: Data
|
||||
|
||||
struct SoftLogoutCredentials {
|
||||
let userId: String
|
||||
let homeserverName: String
|
||||
let userDisplayName: String
|
||||
let deviceId: String?
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum SoftLogoutViewModelAction: CustomStringConvertible {
|
||||
/// Login with password
|
||||
case login(String)
|
||||
/// Forgot password
|
||||
case forgotPassword
|
||||
/// Clear all user data
|
||||
case clearAllData
|
||||
/// Continue using OIDC.
|
||||
case continueWithOIDC
|
||||
|
||||
/// A string representation of the result, ignoring any associated values that could leak PII.
|
||||
var description: String {
|
||||
switch self {
|
||||
case .login:
|
||||
return "login"
|
||||
case .forgotPassword:
|
||||
return "forgotPassword"
|
||||
case .clearAllData:
|
||||
return "clearAllData"
|
||||
case .continueWithOIDC:
|
||||
return "continueWithOIDC"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct SoftLogoutViewState: BindableState {
|
||||
/// Soft logout credentials
|
||||
var credentials: SoftLogoutCredentials
|
||||
|
||||
/// Data about the selected homeserver.
|
||||
var homeserver: LoginHomeserver
|
||||
|
||||
/// Flag indicating soft logged out user needs backup for some keys
|
||||
var keyBackupNeeded: Bool
|
||||
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: SoftLogoutBindings
|
||||
|
||||
/// The types of login supported by the homeserver.
|
||||
var loginMode: LoginMode { homeserver.loginMode }
|
||||
|
||||
/// Whether to show recover encryption keys message
|
||||
var showRecoverEncryptionKeysMessage: Bool {
|
||||
keyBackupNeeded
|
||||
}
|
||||
|
||||
/// `true` when valid credentials have been entered and a homeserver has been loaded.
|
||||
var canSubmit: Bool {
|
||||
!bindings.password.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
struct SoftLogoutBindings {
|
||||
/// The password input by the user.
|
||||
var password: String
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<SoftLogoutErrorType>?
|
||||
}
|
||||
|
||||
enum SoftLogoutViewAction {
|
||||
/// Login.
|
||||
case login
|
||||
/// Forgot password
|
||||
case forgotPassword
|
||||
/// Clear all user data.
|
||||
case clearAllData
|
||||
/// Continue using OIDC.
|
||||
case continueWithOIDC
|
||||
}
|
||||
|
||||
enum SoftLogoutErrorType: Hashable {
|
||||
/// A specific error message shown in an alert.
|
||||
case alert(String)
|
||||
/// An unknown error occurred.
|
||||
case unknown
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
typealias SoftLogoutViewModelType = StateStoreViewModel<SoftLogoutViewState, SoftLogoutViewAction>
|
||||
|
||||
class SoftLogoutViewModel: SoftLogoutViewModelType, SoftLogoutViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: (@MainActor (SoftLogoutViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(credentials: SoftLogoutCredentials,
|
||||
homeserver: LoginHomeserver,
|
||||
keyBackupNeeded: Bool,
|
||||
password: String = "") {
|
||||
let bindings = SoftLogoutBindings(password: password)
|
||||
let viewState = SoftLogoutViewState(credentials: credentials,
|
||||
homeserver: homeserver,
|
||||
keyBackupNeeded: keyBackupNeeded,
|
||||
bindings: bindings)
|
||||
super.init(initialViewState: viewState)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: SoftLogoutViewAction) async {
|
||||
switch viewAction {
|
||||
case .login:
|
||||
callback?(.login(state.bindings.password))
|
||||
case .forgotPassword:
|
||||
callback?(.forgotPassword)
|
||||
case .clearAllData:
|
||||
callback?(.clearAllData)
|
||||
case .continueWithOIDC:
|
||||
callback?(.continueWithOIDC)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor func displayError(_ type: SoftLogoutErrorType) {
|
||||
switch type {
|
||||
case .alert(let message):
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
title: ElementL10n.dialogTitleError,
|
||||
message: message)
|
||||
case .unknown:
|
||||
state.bindings.alertInfo = AlertInfo(id: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SoftLogoutViewModelProtocol {
|
||||
var callback: (@MainActor (SoftLogoutViewModelAction) -> Void)? { get set }
|
||||
var context: SoftLogoutViewModelType.Context { get }
|
||||
|
||||
/// Display an error to the user.
|
||||
@MainActor func displayError(_ type: SoftLogoutErrorType)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SoftLogoutScreen: View {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
/// The focus state of the password text field.
|
||||
@FocusState private var isPasswordFocused: Bool
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var context: SoftLogoutViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
.padding(.top, UIConstants.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 36)
|
||||
|
||||
switch context.viewState.loginMode {
|
||||
case .password:
|
||||
loginForm
|
||||
case .oidc:
|
||||
oidcButton
|
||||
default:
|
||||
loginUnavailableText
|
||||
}
|
||||
|
||||
clearDataForm
|
||||
.padding(.top, 16)
|
||||
}
|
||||
.readableFrame()
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
}
|
||||
|
||||
/// The title, message and icon at the top of the screen.
|
||||
var header: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(ElementL10n.softLogoutSigninTitle)
|
||||
.font(.element.title2Bold)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
Text(ElementL10n.softLogoutSigninNotice(context.viewState.credentials.homeserverName, context.viewState.credentials.userDisplayName, context.viewState.credentials.userId))
|
||||
.font(.element.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("messageLabel1")
|
||||
|
||||
if context.viewState.showRecoverEncryptionKeysMessage {
|
||||
Text(ElementL10n.softLogoutSigninE2eWarningNotice)
|
||||
.font(.element.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("messageLabel2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The form with text fields for username and password, along with a submit button.
|
||||
var loginForm: some View {
|
||||
VStack(spacing: 14) {
|
||||
SecureField(ElementL10n.loginSignupPasswordHint, text: $context.password)
|
||||
.focused($isPasswordFocused)
|
||||
.textFieldStyle(.elementInput())
|
||||
.textContentType(.password)
|
||||
.submitLabel(.done)
|
||||
.onSubmit(submit)
|
||||
.accessibilityIdentifier("passwordTextField")
|
||||
|
||||
Button { context.send(viewAction: .forgotPassword) } label: {
|
||||
Text(ElementL10n.authenticationLoginForgotPassword)
|
||||
.font(.element.body)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.bottom, 8)
|
||||
.accessibilityIdentifier("forgotPasswordButton")
|
||||
|
||||
Button(action: submit) {
|
||||
Text(ElementL10n.loginSignupSubmit)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.disabled(!context.viewState.canSubmit)
|
||||
.accessibilityIdentifier("nextButton")
|
||||
}
|
||||
}
|
||||
|
||||
/// The OIDC button that can be used for login.
|
||||
var oidcButton: some View {
|
||||
Button { context.send(viewAction: .continueWithOIDC) } label: {
|
||||
Text(ElementL10n.loginContinue)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityIdentifier("oidcButton")
|
||||
}
|
||||
|
||||
/// Text shown if neither password or OIDC login is supported.
|
||||
var loginUnavailableText: some View {
|
||||
Text(ElementL10n.autodiscoverWellKnownError)
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityIdentifier("unsupportedServerText")
|
||||
}
|
||||
|
||||
/// The text field and submit button where the user enters an email address.
|
||||
var clearDataForm: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(ElementL10n.softLogoutClearDataTitle)
|
||||
.font(.element.title2Bold)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("clearDataTitleLabel")
|
||||
|
||||
Text(ElementL10n.softLogoutClearDataNotice)
|
||||
.font(.element.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("clearDataMessageLabel")
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Button(action: clearData) {
|
||||
Text(ElementL10n.softLogoutClearDataSubmit)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge, color: .element.alert))
|
||||
.accessibilityIdentifier("clearDataButton")
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the `login` view action so long as a valid email address has been input.
|
||||
func submit() {
|
||||
guard context.viewState.canSubmit else { return }
|
||||
context.send(viewAction: .login)
|
||||
}
|
||||
|
||||
/// Sends the `forgotPassword` view action.
|
||||
func forgotPassword() {
|
||||
context.send(viewAction: .forgotPassword)
|
||||
}
|
||||
|
||||
/// Sends the `clearAllData` view action.
|
||||
func clearData() {
|
||||
context.send(viewAction: .clearAllData)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct SoftLogout_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ForEach(MockSoftLogoutScreenState.allCases) { state in
|
||||
screen(for: state.viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
static func screen(for viewModel: SoftLogoutViewModel) -> some View {
|
||||
NavigationView {
|
||||
SoftLogoutScreen(context: viewModel.context)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.tint(.element.accent)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,8 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
|
||||
self?.viewModel.showSessionVerificationBanner()
|
||||
case .didVerifySession:
|
||||
self?.viewModel.hideSessionVerificationBanner()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
|
||||
@@ -104,11 +104,17 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
|
||||
|
||||
let loginTask: Task<Client, Error> = Task.detached {
|
||||
try self.authenticationService.login(username: username, password: password)
|
||||
#warning("Use new api on next SDK release.")
|
||||
return try self.authenticationService.login(username: username,
|
||||
password: password)
|
||||
// try self.authenticationService.login(username: username,
|
||||
// password: password,
|
||||
// initialDeviceName: initialDeviceName,
|
||||
// deviceId: deviceId)
|
||||
}
|
||||
|
||||
switch await loginTask.result {
|
||||
|
||||
@@ -36,5 +36,5 @@ protocol AuthenticationServiceProxyProtocol {
|
||||
/// Performs login using OIDC for the current homeserver.
|
||||
func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
/// Performs a password login using the current homeserver.
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
.failure(.oidcError(.notSupported))
|
||||
}
|
||||
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
|
||||
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)
|
||||
|
||||
@@ -28,7 +28,7 @@ class ScreenshotDetector {
|
||||
var callback: (@MainActor (UIImage?, Error?) -> Void)?
|
||||
|
||||
/// Flag to whether ask for photos authorization by default if needed.
|
||||
var autoRequestPHAuthorization = true
|
||||
var autoRequestPHAuthorization = false
|
||||
|
||||
init() {
|
||||
startObservingScreenshots()
|
||||
|
||||
@@ -29,6 +29,14 @@ private class WeakClientProxyWrapper: ClientDelegate {
|
||||
func didReceiveSyncUpdate() {
|
||||
clientProxy?.didReceiveSyncUpdate()
|
||||
}
|
||||
|
||||
func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
|
||||
}
|
||||
|
||||
func didUpdateRestoreToken() {
|
||||
clientProxy?.didUpdateRestoreToken()
|
||||
}
|
||||
}
|
||||
|
||||
class ClientProxy: ClientProxyProtocol {
|
||||
@@ -57,11 +65,18 @@ class ClientProxy: ClientProxyProtocol {
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
|
||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||
|
||||
|
||||
#warning("Use isSoftLogout() api on next SDK release.")
|
||||
Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
|
||||
client.startSync(timelineLimit: ClientProxy.syncLimit)
|
||||
|
||||
|
||||
Task { await updateRooms() }
|
||||
// if !client.isSoftLogout() {
|
||||
// Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
|
||||
// client.startSync(timelineLimit: ClientProxy.syncLimit)
|
||||
//
|
||||
// Task { await updateRooms() }
|
||||
// }
|
||||
}
|
||||
|
||||
var userIdentifier: String {
|
||||
@@ -72,6 +87,34 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return "Unknown user identifier"
|
||||
}
|
||||
}
|
||||
|
||||
var isSoftLogout: Bool {
|
||||
#warning("Use isSoftLogout() api on next SDK release.")
|
||||
return false
|
||||
// client.isSoftLogout()
|
||||
}
|
||||
|
||||
var deviceId: String? {
|
||||
do {
|
||||
return try client.deviceId()
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving device id with error: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var homeserver: String {
|
||||
client.homeserver()
|
||||
}
|
||||
|
||||
var restoreToken: String? {
|
||||
do {
|
||||
return try client.restoreToken()
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving restore token with error: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||
await Task.detached {
|
||||
@@ -91,7 +134,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
let avatarURL = try self.client.avatarUrl()
|
||||
return .success(avatarURL)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingDisplayName)
|
||||
return .failure(.failedRetrievingAvatarURL)
|
||||
}
|
||||
}
|
||||
.value
|
||||
@@ -125,6 +168,15 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
do {
|
||||
#warning("Use logout() api on next SDK release.")
|
||||
// try client.logout()
|
||||
} catch {
|
||||
MXLog.error("Failed logging out with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@@ -137,6 +189,14 @@ class ClientProxy: ClientProxyProtocol {
|
||||
await self.updateRooms()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
||||
}
|
||||
|
||||
fileprivate func didUpdateRestoreToken() {
|
||||
callbacks.send(.updatedRestoreToken)
|
||||
}
|
||||
|
||||
private func updateRooms() async {
|
||||
var currentRooms = rooms
|
||||
|
||||
@@ -21,6 +21,8 @@ import MatrixRustSDK
|
||||
enum ClientProxyCallback {
|
||||
case updatedRoomsList
|
||||
case receivedSyncUpdate
|
||||
case receivedAuthError(isSoftLogout: Bool)
|
||||
case updatedRestoreToken
|
||||
}
|
||||
|
||||
enum ClientProxyError: Error {
|
||||
@@ -36,6 +38,14 @@ protocol ClientProxyProtocol {
|
||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||
|
||||
var userIdentifier: String { get }
|
||||
|
||||
var isSoftLogout: Bool { get }
|
||||
|
||||
var deviceId: String? { get }
|
||||
|
||||
var homeserver: String { get }
|
||||
|
||||
var restoreToken: String? { get }
|
||||
|
||||
var rooms: [RoomProxy] { get }
|
||||
|
||||
@@ -52,4 +62,6 @@ protocol ClientProxyProtocol {
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
|
||||
|
||||
func logout() async
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ struct MockClientProxy: ClientProxyProtocol {
|
||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||
|
||||
let userIdentifier: String
|
||||
let isSoftLogout = false
|
||||
let deviceId: String? = nil
|
||||
let homeserver = ""
|
||||
let restoreToken: String? = nil
|
||||
|
||||
let rooms = [RoomProxy]()
|
||||
|
||||
@@ -51,4 +55,8 @@ struct MockClientProxy: ClientProxyProtocol {
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
||||
.failure(.failedRetrievingSessionVerificationController)
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ struct MockUserSession: UserSessionProtocol {
|
||||
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
|
||||
|
||||
var userID: String { clientProxy.userIdentifier }
|
||||
var isSoftLogout: Bool { clientProxy.isSoftLogout }
|
||||
var deviceId: String? { clientProxy.deviceId }
|
||||
var homeserver: String { clientProxy.homeserver }
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
}
|
||||
|
||||
@@ -20,9 +20,14 @@ import Foundation
|
||||
class UserSession: UserSessionProtocol {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var checkForSessionVerificationControllerCancellable: AnyCancellable?
|
||||
private var authErrorCancellable: AnyCancellable?
|
||||
private var restoreTokenUpdateCancellable: AnyCancellable?
|
||||
|
||||
var userID: String { clientProxy.userIdentifier }
|
||||
|
||||
var isSoftLogout: Bool { clientProxy.isSoftLogout }
|
||||
var deviceId: String? { clientProxy.deviceId }
|
||||
var homeserver: String { clientProxy.homeserver }
|
||||
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
@@ -33,6 +38,8 @@ class UserSession: UserSessionProtocol {
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
setupSessionVerificationWatchdog()
|
||||
setupAuthErrorWatchdog()
|
||||
setupRestoreTokenUpdateWatchdog()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -77,4 +84,38 @@ class UserSession: UserSessionProtocol {
|
||||
private func tearDownSessionVerificationControllerWatchdog() {
|
||||
checkForSessionVerificationControllerCancellable = nil
|
||||
}
|
||||
|
||||
// MARK: Auth Error Watchdog
|
||||
|
||||
private func setupAuthErrorWatchdog() {
|
||||
authErrorCancellable = clientProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
if case .receivedAuthError(let isSoftLogout) = callback {
|
||||
self?.callbacks.send(.didReceiveAuthError(isSoftLogout: isSoftLogout))
|
||||
self?.tearDownAuthErrorWatchdog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownAuthErrorWatchdog() {
|
||||
authErrorCancellable = nil
|
||||
}
|
||||
|
||||
// MARK: Restore Token Update Watchdog
|
||||
|
||||
private func setupRestoreTokenUpdateWatchdog() {
|
||||
restoreTokenUpdateCancellable = clientProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
if case .updatedRestoreToken = callback {
|
||||
self?.callbacks.send(.updateRestoreTokenNeeded)
|
||||
self?.tearDownRestoreTokenUpdateWatchdog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func tearDownRestoreTokenUpdateWatchdog() {
|
||||
restoreTokenUpdateCancellable = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,15 @@ import Foundation
|
||||
enum UserSessionCallback {
|
||||
case sessionVerificationNeeded
|
||||
case didVerifySession
|
||||
case didReceiveAuthError(isSoftLogout: Bool)
|
||||
case updateRestoreTokenNeeded
|
||||
}
|
||||
|
||||
protocol UserSessionProtocol {
|
||||
var userID: String { get }
|
||||
var isSoftLogout: Bool { get }
|
||||
var deviceId: String? { get }
|
||||
var homeserver: String { get }
|
||||
|
||||
var clientProxy: ClientProxyProtocol { get }
|
||||
var mediaProvider: MediaProviderProtocol { get }
|
||||
|
||||
@@ -69,6 +69,16 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshRestoreToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError> {
|
||||
guard let accessToken = userSession.clientProxy.restoreToken else {
|
||||
return .failure(.failedRefreshingRestoreToken)
|
||||
}
|
||||
|
||||
keychainController.setRestoreToken(accessToken, forUsername: userSession.clientProxy.userIdentifier)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func logout(userSession: UserSessionProtocol) {
|
||||
let userID = userSession.clientProxy.userIdentifier
|
||||
|
||||
@@ -21,6 +21,7 @@ enum UserSessionStoreError: Error {
|
||||
case missingCredentials
|
||||
case failedRestoringLogin
|
||||
case failedSettingUpSession
|
||||
case failedRefreshingRestoreToken
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -36,6 +37,9 @@ protocol UserSessionStoreProtocol {
|
||||
|
||||
/// Creates a user session for a new client from the SDK.
|
||||
func userSession(for client: Client) async -> Result<UserSession, UserSessionStoreError>
|
||||
|
||||
/// Refresh the restore token of the client for a given session.
|
||||
func refreshRestoreToken(for userSession: UserSessionProtocol) -> Result<Void, UserSessionStoreError>
|
||||
|
||||
/// Logs out of the specified session.
|
||||
func logout(userSession: UserSessionProtocol)
|
||||
|
||||
@@ -21,6 +21,7 @@ enum UITestScreenIdentifier: String {
|
||||
case serverSelection
|
||||
case serverSelectionNonModal
|
||||
case authenticationFlow
|
||||
case softLogout
|
||||
case analyticsPrompt
|
||||
case simpleRegular
|
||||
case simpleUpgrade
|
||||
|
||||
@@ -82,6 +82,14 @@ class MockScreen: Identifiable {
|
||||
case .authenticationFlow:
|
||||
return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||
navigationRouter: navigationRouter)
|
||||
case .softLogout:
|
||||
let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org",
|
||||
homeserverName: "matrix.org",
|
||||
userDisplayName: "mock",
|
||||
deviceId: "ABCDEFGH")
|
||||
return SoftLogoutCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
|
||||
credentials: credentials,
|
||||
keyBackupNeeded: false))
|
||||
case .simpleRegular:
|
||||
return TemplateCoordinator(parameters: .init(promptType: .regular))
|
||||
case .simpleUpgrade:
|
||||
|
||||
Reference in New Issue
Block a user