mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge branch 'develop' into ismail/6175_signout_from_all
This commit is contained in:
commit
952a84bc98
28 changed files with 1499 additions and 151 deletions
29
Riot/Categories/MXFileStore.swift
Normal file
29
Riot/Categories/MXFileStore.swift
Normal file
|
@ -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 Foundation
|
||||
|
||||
extension MXFileStore {
|
||||
|
||||
func displayName(ofUserWithId userId: String) async -> String? {
|
||||
await withCheckedContinuation({ continuation in
|
||||
asyncUsers(withUserIds: [userId]) { users in
|
||||
continuation.resume(returning: users.first?.displayname)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,8 @@ enum AuthenticationCoordinatorResult {
|
|||
case didLogin(session: MXSession, authenticationFlow: AuthenticationFlow, authenticationType: AuthenticationType)
|
||||
/// All of the required authentication steps including key verification is complete.
|
||||
case didComplete
|
||||
/// In case of soft logout, user has decided to clear all data
|
||||
case clearAllData
|
||||
/// The user has cancelled the associated authentication flow.
|
||||
case cancel(AuthenticationFlow)
|
||||
}
|
||||
|
@ -40,9 +42,6 @@ protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
|
|||
/// Update the screen to display registration or login.
|
||||
func update(authenticationFlow: AuthenticationFlow)
|
||||
|
||||
/// Update the screen to use any credentials to use after a soft logout has taken place.
|
||||
func update(softLogoutCredentials: MXCredentials)
|
||||
|
||||
/// Indicates to the coordinator to display any pending screens if it was created with
|
||||
/// the `canPresentAdditionalScreens` parameter set to `false`
|
||||
func presentPendingScreensIfNecessary()
|
||||
|
|
|
@ -37,6 +37,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
|
|||
private var canPresentAdditionalScreens: Bool
|
||||
private var isWaitingToPresentCompleteSecurity = false
|
||||
private var verificationListener: SessionVerificationListener?
|
||||
private let authenticationService: AuthenticationService = .shared
|
||||
|
||||
/// The session created when successfully authenticated.
|
||||
private var session: MXSession?
|
||||
|
@ -61,6 +62,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
|
|||
self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens
|
||||
|
||||
let authenticationViewController = AuthenticationViewController()
|
||||
authenticationViewController.softLogoutCredentials = authenticationService.softLogoutCredentials
|
||||
self.authenticationViewController = authenticationViewController
|
||||
|
||||
// Preload the view as this can a second and lock up the UI at presentation.
|
||||
|
@ -87,10 +89,6 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
|
|||
authenticationViewController.authType = authenticationFlow.mxkType
|
||||
}
|
||||
|
||||
func update(softLogoutCredentials: MXCredentials) {
|
||||
authenticationViewController.softLogoutCredentials = softLogoutCredentials
|
||||
}
|
||||
|
||||
func presentPendingScreensIfNecessary() {
|
||||
canPresentAdditionalScreens = true
|
||||
|
||||
|
|
|
@ -872,7 +872,7 @@ static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
|
|||
|
||||
- (void)logout:(void (^)(void))completion
|
||||
{
|
||||
if (!mxSession)
|
||||
if (!mxSession || !mxSession.matrixRestClient)
|
||||
{
|
||||
MXLogDebug(@"[MXKAccount] logout: Need to open the closed session to make a logout request");
|
||||
id<MXStore> store = [[[MXKAccountManager sharedManager].storeClass alloc] init];
|
||||
|
@ -959,6 +959,12 @@ static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
|
|||
|
||||
- (void)softLogout
|
||||
{
|
||||
if (_isSoftLogout)
|
||||
{
|
||||
// do not close the session if already soft logged out
|
||||
// it may break the current logout request and resetting session credentials can cause crashes
|
||||
return;
|
||||
}
|
||||
_isSoftLogout = YES;
|
||||
[[MXKAccountManager sharedManager] saveAccounts];
|
||||
|
||||
|
|
|
@ -116,6 +116,20 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
|
||||
/// Starts the authentication flow.
|
||||
@MainActor private func startAuthenticationFlow() async {
|
||||
if let softLogoutCredentials = authenticationService.softLogoutCredentials,
|
||||
let homeserverAddress = softLogoutCredentials.homeServer {
|
||||
do {
|
||||
try await authenticationService.startFlow(.login, for: homeserverAddress)
|
||||
} catch {
|
||||
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
|
||||
displayError(message: error.localizedDescription)
|
||||
}
|
||||
|
||||
await showSoftLogoutScreen(softLogoutCredentials)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
|
||||
if initialScreen != .selectServerForRegistration {
|
||||
do {
|
||||
|
@ -232,6 +246,54 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the soft logout screen.
|
||||
@MainActor private func showSoftLogoutScreen(_ credentials: MXCredentials) async {
|
||||
MXLog.debug("[AuthenticationCoordinator] showSoftLogoutScreen")
|
||||
|
||||
guard let userId = credentials.userId else {
|
||||
MXLog.failure("[AuthenticationCoordinator] showSoftLogoutScreen: Missing userId.")
|
||||
displayError(message: VectorL10n.errorCommonMessage)
|
||||
return
|
||||
}
|
||||
|
||||
let store = MXFileStore(credentials: credentials)
|
||||
let userDisplayName = await store.displayName(ofUserWithId: userId) ?? ""
|
||||
|
||||
let cryptoStore = MXRealmCryptoStore(credentials: credentials)
|
||||
let keyBackupNeeded = (cryptoStore?.inboundGroupSessions(toBackup: 1) ?? []).count > 0
|
||||
|
||||
let softLogoutCredentials = SoftLogoutCredentials(userId: userId,
|
||||
homeserverName: credentials.homeServerName() ?? "",
|
||||
userDisplayName: userDisplayName,
|
||||
deviceId: credentials.deviceId)
|
||||
|
||||
let parameters = AuthenticationSoftLogoutCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
authenticationService: authenticationService,
|
||||
credentials: softLogoutCredentials,
|
||||
keyBackupNeeded: keyBackupNeeded)
|
||||
let coordinator = AuthenticationSoftLogoutCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .success(let session, let loginPassword):
|
||||
self.password = loginPassword
|
||||
self.authenticationType = .password
|
||||
self.onSessionCreated(session: session, flow: .login)
|
||||
case .clearAllData:
|
||||
self.callback?(.clearAllData)
|
||||
case .continueWithSSO(let provider):
|
||||
self.presentSSOAuthentication(for: provider)
|
||||
case .fallback:
|
||||
self.showFallback(for: .login, deviceId: softLogoutCredentials.deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
navigationRouter.setRootModule(coordinator, popCompletion: nil)
|
||||
}
|
||||
|
||||
/// Displays the next view in the flow based on the result from the registration screen.
|
||||
@MainActor private func loginCoordinator(_ coordinator: AuthenticationLoginCoordinator,
|
||||
|
@ -503,8 +565,26 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
|
||||
// MARK: - Additional Screens
|
||||
|
||||
private func showFallback(for flow: AuthenticationFlow) {
|
||||
let url = authenticationService.fallbackURL(for: flow)
|
||||
private func showFallback(for flow: AuthenticationFlow, deviceId: String? = nil) {
|
||||
var url = authenticationService.fallbackURL(for: flow)
|
||||
|
||||
if let deviceId = deviceId {
|
||||
// add deviceId as `device_id` into the url
|
||||
guard var urlComponents = URLComponents(string: url.absoluteString) else {
|
||||
MXLog.error("[AuthenticationCoordinator] showFallback: could not create url components")
|
||||
return
|
||||
}
|
||||
var queryItems = urlComponents.queryItems ?? []
|
||||
queryItems.append(URLQueryItem(name: "device_id", value: deviceId))
|
||||
urlComponents.queryItems = queryItems
|
||||
|
||||
if let newUrl = urlComponents.url {
|
||||
url = newUrl
|
||||
} else {
|
||||
MXLog.error("[AuthenticationCoordinator] showFallback: could not create url from components")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
MXLog.debug("[AuthenticationCoordinator] showFallback for: \(flow), url: \(url)")
|
||||
|
||||
|
@ -717,9 +797,6 @@ extension AuthenticationCoordinator {
|
|||
// unused
|
||||
}
|
||||
|
||||
func update(softLogoutCredentials: MXCredentials) {
|
||||
// unused
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AuthFallBackViewControllerDelegate
|
||||
|
|
|
@ -17,19 +17,16 @@
|
|||
*/
|
||||
|
||||
import UIKit
|
||||
import CommonKit
|
||||
|
||||
/// OnboardingCoordinator input parameters
|
||||
struct OnboardingCoordinatorParameters {
|
||||
|
||||
/// The navigation router that manage physical navigation
|
||||
let router: NavigationRouterType
|
||||
/// The credentials to use if a soft logout has taken place.
|
||||
let softLogoutCredentials: MXCredentials?
|
||||
|
||||
init(router: NavigationRouterType? = nil,
|
||||
softLogoutCredentials: MXCredentials? = nil) {
|
||||
init(router: NavigationRouterType? = nil) {
|
||||
self.router = router ?? NavigationRouter(navigationController: RiotNavigationController(isLockedToPortraitOnPhone: true))
|
||||
self.softLogoutCredentials = softLogoutCredentials
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,6 +62,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
private var session: MXSession?
|
||||
/// A place to store the image selected in the avatar screen until it has been saved.
|
||||
private var selectedAvatar: UIImage?
|
||||
private let authenticationService: AuthenticationService = .shared
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
private var shouldShowDisplayNameScreen = false
|
||||
private var shouldShowAvatarScreen = false
|
||||
|
@ -86,8 +87,11 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
self.parameters = parameters
|
||||
|
||||
// Preload the legacy authVC (it is *really* slow to load in realtime)
|
||||
let authenticationParameters = LegacyAuthenticationCoordinatorParameters(navigationRouter: parameters.router, canPresentAdditionalScreens: false)
|
||||
legacyAuthenticationCoordinator = LegacyAuthenticationCoordinator(parameters: authenticationParameters)
|
||||
let params = LegacyAuthenticationCoordinatorParameters(navigationRouter: parameters.router,
|
||||
canPresentAdditionalScreens: false)
|
||||
legacyAuthenticationCoordinator = LegacyAuthenticationCoordinator(parameters: params)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.router.toPresentable())
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
@ -95,8 +99,22 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
// TODO: Manage a separate flow for soft logout that just uses AuthenticationCoordinator
|
||||
if parameters.softLogoutCredentials == nil, BuildSettings.authScreenShowRegister {
|
||||
if authenticationService.softLogoutCredentials != nil {
|
||||
// show the splash screen and a loading indicator
|
||||
if BuildSettings.authScreenShowRegister {
|
||||
showSplashScreen()
|
||||
} else {
|
||||
showEmptyScreen()
|
||||
}
|
||||
startLoading()
|
||||
if BuildSettings.onboardingEnableNewAuthenticationFlow {
|
||||
beginAuthentication(with: .login) { [weak self] in
|
||||
self?.stopLoading()
|
||||
}
|
||||
} else {
|
||||
showLegacyAuthenticationScreen(forceAsRootModule: true)
|
||||
}
|
||||
} else if BuildSettings.authScreenShowRegister {
|
||||
showSplashScreen()
|
||||
} else {
|
||||
showLegacyAuthenticationScreen()
|
||||
|
@ -126,6 +144,15 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
/// Show an empty screen when configuring soft logout flow
|
||||
private func showEmptyScreen() {
|
||||
MXLog.debug("[OnboardingCoordinator] showEmptyScreen")
|
||||
|
||||
let viewController = UIViewController()
|
||||
viewController.view.backgroundColor = ThemeService.shared().theme.backgroundColor
|
||||
navigationRouter.setRootModule(viewController)
|
||||
}
|
||||
|
||||
/// Displays the next view in the flow after the splash screen.
|
||||
private func splashScreenCoordinator(_ coordinator: OnboardingSplashScreenCoordinator, didCompleteWith result: OnboardingSplashScreenViewModelResult) {
|
||||
|
@ -191,7 +218,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
// MARK: - Authentication
|
||||
|
||||
/// Show the authentication flow, starting at the specified initial screen.
|
||||
private func beginAuthentication(with initialScreen: AuthenticationCoordinator.EntryPoint, onStart: @escaping () -> Void) {
|
||||
private func beginAuthentication(with initialScreen: AuthenticationCoordinator.EntryPoint, onStart: (() -> Void)? = nil) {
|
||||
MXLog.debug("[OnboardingCoordinator] beginAuthentication")
|
||||
|
||||
let parameters = AuthenticationCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
|
@ -203,11 +230,20 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
|
||||
switch result {
|
||||
case .didStart:
|
||||
onStart()
|
||||
onStart?()
|
||||
case .didLogin(let session, let authenticationFlow, let authenticationType):
|
||||
self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType)
|
||||
case .didComplete:
|
||||
self.authenticationCoordinatorDidComplete(coordinator)
|
||||
case .clearAllData:
|
||||
self.showClearAllDataConfirmation {
|
||||
MXLog.debug("[OnboardingCoordinator] beginAuthentication: clear all data after soft logout")
|
||||
self.authenticationService.reset()
|
||||
self.isShowingLegacyAuthentication = false
|
||||
self.authenticationFinished = false
|
||||
self.cancelAuthentication(flow: .login)
|
||||
AppDelegate.theDelegate().logoutSendingRequestServer(true, completion: nil)
|
||||
}
|
||||
case .cancel(let flow):
|
||||
self.cancelAuthentication(flow: flow)
|
||||
}
|
||||
|
@ -217,9 +253,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
|
||||
/// Show the legacy authentication screen. Any parameters that have been set in previous screens are be applied.
|
||||
private func showLegacyAuthenticationScreen() {
|
||||
/// - Parameter forceAsRootModule: Force setting the module as root instead of pushing
|
||||
private func showLegacyAuthenticationScreen(forceAsRootModule: Bool = false) {
|
||||
guard !isShowingLegacyAuthentication else { return }
|
||||
|
||||
MXLog.debug("[OnboardingCoordinator] showLegacyAuthenticationScreen")
|
||||
|
@ -233,24 +270,20 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType)
|
||||
case .didComplete:
|
||||
self.authenticationCoordinatorDidComplete(coordinator)
|
||||
case .didStart, .cancel:
|
||||
case .didStart, .clearAllData, .cancel:
|
||||
// These results are only sent by the new flow.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.customServerFieldsVisible = useCaseResult == .customServer
|
||||
|
||||
if let softLogoutCredentials = parameters.softLogoutCredentials {
|
||||
coordinator.update(softLogoutCredentials: softLogoutCredentials)
|
||||
}
|
||||
|
||||
|
||||
authenticationCoordinator = coordinator
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
if navigationRouter.modules.isEmpty {
|
||||
if navigationRouter.modules.isEmpty || forceAsRootModule {
|
||||
navigationRouter.setRootModule(coordinator, popCompletion: nil)
|
||||
} else {
|
||||
navigationRouter.push(coordinator, animated: true) { [weak self] in
|
||||
|
@ -259,6 +292,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
}
|
||||
}
|
||||
isShowingLegacyAuthentication = true
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
/// Cancels the registration flow, returning to the Use Case screen.
|
||||
|
@ -270,8 +304,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
showSplashScreen()
|
||||
showUseCaseSelectionScreen(animated: false)
|
||||
case .login:
|
||||
// Probably not needed, error for now until the new login flow is implemented.
|
||||
MXLog.failure("[OnboardingCoordinator] cancelAuthentication: Not implemented for the login flow")
|
||||
navigationRouter.popAllModules(animated: false)
|
||||
|
||||
showSplashScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -563,6 +598,29 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
|
||||
Analytics.shared.trackSignup(authenticationType: authenticationType.analyticsType)
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
private func startLoading() {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
/// Show confirmation to clear all data
|
||||
/// - Parameter confirmed: Callback to be called when confirmed.
|
||||
private func showClearAllDataConfirmation(_ confirmed: (() -> Void)?) {
|
||||
let alertController = UIAlertController(title: VectorL10n.authSoftlogoutClearDataSignOutTitle,
|
||||
message: VectorL10n.authSoftlogoutClearDataSignOutMsg,
|
||||
preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: VectorL10n.cancel, style: .cancel, handler: nil))
|
||||
alertController.addAction(UIAlertAction(title: VectorL10n.authSoftlogoutClearDataSignOut, style: .default, handler: { action in
|
||||
confirmed?()
|
||||
}))
|
||||
navigationRouter.present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
|
|
@ -18,12 +18,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@objcMembers
|
||||
class OnboardingCoordinatorBridgePresenterParameters: NSObject {
|
||||
/// The credentials to use after a soft logout has taken place.
|
||||
var softLogoutCredentials: MXCredentials?
|
||||
}
|
||||
|
||||
/// OnboardingCoordinatorBridgePresenter enables to start OnboardingCoordinator from a view controller.
|
||||
/// This bridge is used while waiting for global usage of coordinator pattern.
|
||||
/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). Each bridge should be removed
|
||||
|
@ -42,7 +36,6 @@ final class OnboardingCoordinatorBridgePresenter: NSObject {
|
|||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: OnboardingCoordinatorBridgePresenterParameters
|
||||
private var navigationType: NavigationType = .present
|
||||
private var coordinator: OnboardingCoordinator?
|
||||
|
||||
|
@ -50,12 +43,6 @@ final class OnboardingCoordinatorBridgePresenter: NSObject {
|
|||
|
||||
var completion: (() -> Void)?
|
||||
|
||||
// MARK: Setup
|
||||
init(with parameters: OnboardingCoordinatorBridgePresenterParameters) {
|
||||
self.parameters = parameters
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func present(from viewController: UIViewController, animated: Bool) {
|
||||
|
@ -117,8 +104,7 @@ final class OnboardingCoordinatorBridgePresenter: NSObject {
|
|||
|
||||
/// Makes an `OnboardingCoordinator` using the supplied navigation router, or creating one if needed.
|
||||
private func makeOnboardingCoordinator(navigationRouter: NavigationRouterType? = nil) -> OnboardingCoordinator {
|
||||
let onboardingCoordinatorParameters = OnboardingCoordinatorParameters(router: navigationRouter,
|
||||
softLogoutCredentials: parameters.softLogoutCredentials)
|
||||
let onboardingCoordinatorParameters = OnboardingCoordinatorParameters(router: navigationRouter)
|
||||
|
||||
let onboardingCoordinator = OnboardingCoordinator(parameters: onboardingCoordinatorParameters)
|
||||
onboardingCoordinator.completion = { [weak self] in
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/// Account deactivation parameters for password deactivation.
|
||||
struct DeactivateAccountPasswordParameters: DictionaryEncodable {
|
||||
/// The type of authentication being used.
|
||||
let type = kMXLoginFlowTypePassword
|
||||
/// The account's matrix ID.
|
||||
let user: String
|
||||
/// The account's password.
|
||||
let password: String
|
||||
}
|
||||
|
||||
/// Account deactivation parameters for use after fallback authentication has completed.
|
||||
struct DeactivateAccountDummyParameters: DictionaryEncodable {
|
||||
/// The type of authentication being used.
|
||||
let type = kMXLoginFlowTypeDummy
|
||||
/// The account's matrix ID.
|
||||
let user: String
|
||||
/// The session ID used when completing authentication.
|
||||
let session: String
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
@objc protocol DeactivateAccountServiceDelegate: AnyObject {
|
||||
/// The service encountered an error.
|
||||
/// - Parameter error: The error that occurred.
|
||||
func deactivateAccountServiceDidEncounterError(_ error: Error)
|
||||
|
||||
/// The service successfully completed the account deactivation.
|
||||
func deactivateAccountServiceDidCompleteDeactivation()
|
||||
}
|
||||
|
||||
/// The kind of authentication needed to deactivate an account.
|
||||
@objc enum DeactivateAccountAuthentication: Int {
|
||||
/// The deactivation endpoint is already authenticated. This is unlikely to be the case.
|
||||
case authenticated
|
||||
/// The deactivation endpoint requires a password for authentication.
|
||||
case requiresPassword
|
||||
/// The deactivation endpoint requires fallback for authentication.
|
||||
case requiresFallback
|
||||
}
|
||||
|
||||
/// An error that occurred in the `DeactivateAccountService`
|
||||
enum DeactivateAccountServiceError: Error {
|
||||
/// The next stage in the flow wasn't found.
|
||||
case missingStage
|
||||
/// The URL needed to present fallback authentication wasn't found.
|
||||
case missingFallbackURL
|
||||
}
|
||||
|
||||
/// A service that helps handle interactive authentication when deactivating an account.
|
||||
@objcMembers class DeactivateAccountService: NSObject {
|
||||
private let session: MXSession
|
||||
private let uiaService: UserInteractiveAuthenticationService
|
||||
private let request = AuthenticatedEndpointRequest(path: "\(kMXAPIPrefixPathR0)/account/deactivate", httpMethod: "POST")
|
||||
|
||||
/// The authentication session's ID if interactive authentication has begun, otherwise `nil`.
|
||||
private var sessionID: String?
|
||||
|
||||
weak var delegate: DeactivateAccountServiceDelegate?
|
||||
|
||||
/// Creates a new service for the supplied session.
|
||||
/// - Parameter session: The session with the account to be deactivated.
|
||||
init(session: MXSession) {
|
||||
self.session = session
|
||||
self.uiaService = UserInteractiveAuthenticationService(session: session)
|
||||
}
|
||||
|
||||
/// Checks the authentication required for deactivation.
|
||||
/// - Parameters:
|
||||
/// - success: A closure called containing information about the kind of authentication required (and a fallback URL if needed).
|
||||
/// - failure: A closure called then the check failed.
|
||||
func checkAuthentication(success: @escaping (DeactivateAccountAuthentication, URL?) -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
uiaService.authenticatedEndpointStatus(for: request) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let status):
|
||||
switch status {
|
||||
case .authenticationNotNeeded:
|
||||
success(.authenticated, nil)
|
||||
case .authenticationNeeded(let session):
|
||||
do {
|
||||
let (authentication, fallbackURL) = try self.handleAuthenticationSession(session)
|
||||
success(authentication, fallbackURL)
|
||||
} catch {
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the authentication information out of an `MXAuthenticationSession`.
|
||||
/// - Parameter authenticationSession: The authentication session to be used.
|
||||
/// - Returns: A tuple containing the required authentication method along with a URL for fallback if necessary.
|
||||
private func handleAuthenticationSession(_ authenticationSession: MXAuthenticationSession) throws -> (DeactivateAccountAuthentication, URL?) {
|
||||
guard let nextStage = uiaService.firstUncompletedFlowIdentifier(in: authenticationSession) else {
|
||||
MXLog.error("[DeactivateAccountService] handleAuthenticationSession: Failed to determine the next stage.")
|
||||
throw DeactivateAccountServiceError.missingStage
|
||||
}
|
||||
|
||||
switch nextStage {
|
||||
case kMXLoginFlowTypePassword:
|
||||
sessionID = authenticationSession.session
|
||||
return (.requiresPassword, nil)
|
||||
default:
|
||||
guard let fallbackURL = uiaService.firstUncompletedStageAuthenticationFallbackURL(for: authenticationSession) else {
|
||||
MXLog.error("[DeactivateAccountService] handleAuthenticationSession: Failed to determine fallback URL.")
|
||||
throw DeactivateAccountServiceError.missingFallbackURL
|
||||
}
|
||||
sessionID = authenticationSession.session
|
||||
return (.requiresFallback, fallbackURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Deactivates the account with the supplied password.
|
||||
/// - Parameters:
|
||||
/// - password: The password for the account.
|
||||
/// - eraseAccount: Whether or not to erase all of the data from the account too.
|
||||
func deactivate(with password: String, eraseAccount: Bool) {
|
||||
do {
|
||||
let parameters = try DeactivateAccountPasswordParameters(user: session.myUserId, password: password).dictionary()
|
||||
deactivateAccount(parameters: parameters, eraseAccount: eraseAccount)
|
||||
} catch {
|
||||
self.delegate?.deactivateAccountServiceDidEncounterError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deactivates the account when authentication has already been completed.
|
||||
/// - Parameter eraseAccount: Whether or not to erase all of the data from the account too.
|
||||
func deactivate(eraseAccount: Bool) {
|
||||
do {
|
||||
let parameters = try DeactivateAccountDummyParameters(user: session.myUserId, session: sessionID ?? "").dictionary()
|
||||
deactivateAccount(parameters: parameters, eraseAccount: eraseAccount)
|
||||
} catch {
|
||||
self.delegate?.deactivateAccountServiceDidEncounterError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Deactivated the account using the specified parameters.
|
||||
/// - Parameters:
|
||||
/// - parameters: The parameters for account deactivation.
|
||||
/// - eraseAccount: Whether or not to erase all of the data from the account too.
|
||||
private func deactivateAccount(parameters: [String: Any], eraseAccount: Bool) {
|
||||
session.deactivateAccount(withAuthParameters: parameters, eraseAccount: eraseAccount) { [weak self] response in
|
||||
switch response {
|
||||
case .success:
|
||||
self?.delegate?.deactivateAccountServiceDidCompleteDeactivation()
|
||||
case .failure(let error):
|
||||
self?.delegate?.deactivateAccountServiceDidEncounterError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
#import "DeactivateAccountViewController.h"
|
||||
|
||||
#import <SafariServices/SafariServices.h>
|
||||
#import "ThemeService.h"
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
|
||||
|
@ -26,7 +27,7 @@ static CGFloat const kTextFontSize = 15.0;
|
|||
|
||||
#pragma mark - Private Interface
|
||||
|
||||
@interface DeactivateAccountViewController ()
|
||||
@interface DeactivateAccountViewController () <DeactivateAccountServiceDelegate, SFSafariViewControllerDelegate>
|
||||
|
||||
#pragma mark - Outlets
|
||||
|
||||
|
@ -49,6 +50,8 @@ static CGFloat const kTextFontSize = 15.0;
|
|||
|
||||
@property (nonatomic) AnalyticsScreenTracker *screenTracker;
|
||||
|
||||
@property (nonatomic) DeactivateAccountService *deactivateAccountService;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Implementation
|
||||
|
@ -96,6 +99,9 @@ static CGFloat const kTextFontSize = 15.0;
|
|||
|
||||
self.errorPresentation = [[MXKErrorAlertPresentation alloc] init];
|
||||
[self registerThemeNotification];
|
||||
|
||||
self.deactivateAccountService = [[DeactivateAccountService alloc] initWithSession:self.mainSession];
|
||||
self.deactivateAccountService.delegate = self;
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
|
@ -266,55 +272,22 @@ static CGFloat const kTextFontSize = 15.0;
|
|||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)deactivateAccountWithUserId:(NSString*)userId
|
||||
andPassword:(NSString*)password
|
||||
eraseAllMessages:(BOOL)eraseAllMessages
|
||||
- (void)startLoading
|
||||
{
|
||||
if (password && userId)
|
||||
{
|
||||
[self enableUserActions:NO];
|
||||
[self startActivityIndicator];
|
||||
|
||||
// This assumes that the homeserver requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
|
||||
NSDictionary *authParameters = @{@"user": userId,
|
||||
@"password": password,
|
||||
@"type": kMXLoginFlowTypePassword};
|
||||
|
||||
[self.mainSession deactivateAccountWithAuthParameters:authParameters eraseAccount:eraseAllMessages success:^{
|
||||
MXLogDebug(@"[SettingsViewController] Deactivate account with success");
|
||||
|
||||
typeof(weakSelf) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf)
|
||||
{
|
||||
[strongSelf stopActivityIndicator];
|
||||
[strongSelf enableUserActions:YES];
|
||||
[strongSelf.delegate deactivateAccountViewControllerDidDeactivateWithSuccess:strongSelf];
|
||||
}
|
||||
|
||||
} failure:^(NSError *error) {
|
||||
|
||||
MXLogDebug(@"[SettingsViewController] Failed to deactivate account");
|
||||
|
||||
typeof(weakSelf) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf)
|
||||
{
|
||||
[strongSelf stopActivityIndicator];
|
||||
[strongSelf enableUserActions:YES];
|
||||
[strongSelf.errorPresentation presentErrorFromViewController:strongSelf forError:error animated:YES handler:nil];
|
||||
}
|
||||
}];
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"[SettingsViewController] Failed to deactivate account");
|
||||
[self.errorPresentation presentGenericErrorFromViewController:self animated:YES handler:nil];
|
||||
}
|
||||
[self enableUserActions:NO];
|
||||
[self startActivityIndicator];
|
||||
}
|
||||
|
||||
- (void)stopLoading
|
||||
{
|
||||
[self stopActivityIndicator];
|
||||
[self enableUserActions:YES];
|
||||
}
|
||||
|
||||
- (void)handleError:(NSError *)error
|
||||
{
|
||||
[self stopLoading];
|
||||
[self.errorPresentation presentErrorFromViewController:self forError:error animated:YES handler:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
@ -331,19 +304,78 @@ static CGFloat const kTextFontSize = 15.0;
|
|||
|
||||
- (IBAction)deactivateAccountButtonAction:(id)sender
|
||||
{
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self startLoading];
|
||||
|
||||
[self presentPasswordRequiredAlertWithSubmitHandler:^(NSString *password) {
|
||||
MXWeakify(self);
|
||||
[self.deactivateAccountService checkAuthenticationWithSuccess:^(enum DeactivateAccountAuthentication authentication, NSURL * _Nullable fallbackURL) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
typeof(weakSelf) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf)
|
||||
{
|
||||
NSString *userId = strongSelf.mainSession.myUser.userId;
|
||||
[strongSelf deactivateAccountWithUserId:userId andPassword:password eraseAllMessages:strongSelf.forgetMessageButton.isEnabled];
|
||||
switch (authentication) {
|
||||
case DeactivateAccountAuthenticationAuthenticated:
|
||||
MXLogDebug(@"[DeactivateAccountViewController] Deactivation endpoint has already been authenticated. Continuing deactivation.")
|
||||
[self.deactivateAccountService deactivateWithEraseAccount:self.forgetMessageButton.isSelected];
|
||||
break;
|
||||
case DeactivateAccountAuthenticationRequiresPassword:
|
||||
[self presentPasswordPrompt];
|
||||
break;
|
||||
case DeactivateAccountAuthenticationRequiresFallback:
|
||||
if (fallbackURL) [self presentFallbackForURL:fallbackURL];
|
||||
break;
|
||||
}
|
||||
|
||||
} cancelHandler:nil];
|
||||
} failure:^(NSError * _Nonnull error) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self handleError:error];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Password
|
||||
|
||||
- (void)presentPasswordPrompt
|
||||
{
|
||||
MXLogDebug(@"[DeactivateAccountViewController] Show password prompt.")
|
||||
|
||||
MXWeakify(self);
|
||||
[self presentPasswordRequiredAlertWithSubmitHandler:^(NSString *password) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self.deactivateAccountService deactivateWith:password eraseAccount:self.forgetMessageButton.isSelected];
|
||||
} cancelHandler:^() {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self stopLoading];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Fallback
|
||||
|
||||
- (void)presentFallbackForURL:(NSURL *)url
|
||||
{
|
||||
MXLogDebug(@"[DeactivateAccountViewController] Show fallback for url: %@", url)
|
||||
SFSafariViewController *safariViewController = [[SFSafariViewController alloc] initWithURL:url];
|
||||
safariViewController.modalPresentationStyle = UIModalPresentationFormSheet;
|
||||
safariViewController.delegate = self;
|
||||
|
||||
[self presentViewController:safariViewController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller
|
||||
{
|
||||
// There is no indication from the fallback page or the UIAService whether this was successful so attempt to deactivate.
|
||||
// It will fail (and display an error to the user) if the fallback page was dismissed.
|
||||
MXLogDebug(@"[DeactivateAccountViewController] safariViewControllerDidFinish: Completing deactivation after fallback.")
|
||||
[self.deactivateAccountService deactivateWithEraseAccount:self.forgetMessageButton.isSelected];
|
||||
}
|
||||
|
||||
#pragma mark - DeactivateAccountServiceDelegate
|
||||
|
||||
- (void)deactivateAccountServiceDidEncounterError:(NSError *)error
|
||||
{
|
||||
MXLogDebug(@"[DeactivateAccountViewController] Failed to deactivate account");
|
||||
[self handleError:error];
|
||||
}
|
||||
|
||||
- (void)deactivateAccountServiceDidCompleteDeactivation
|
||||
{
|
||||
MXLogDebug(@"[DeactivateAccountViewController] Deactivate account with success");
|
||||
[self.delegate deactivateAccountViewControllerDidDeactivateWithSuccess:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -67,8 +67,6 @@
|
|||
@property (nonatomic, readwrite) id addAccountObserver;
|
||||
@property (nonatomic, readwrite) id removeAccountObserver;
|
||||
|
||||
@property (nonatomic, readwrite) MXCredentials *softLogoutCredentials;
|
||||
|
||||
@property (nonatomic) BOOL reviewSessionAlertHasBeenDisplayed;
|
||||
|
||||
@end
|
||||
|
@ -475,15 +473,10 @@
|
|||
// TODO: Manage the onboarding coordinator at the AppCoordinator level
|
||||
- (void)presentOnboardingFlow
|
||||
{
|
||||
OnboardingCoordinatorBridgePresenterParameters *parameters = [[OnboardingCoordinatorBridgePresenterParameters alloc] init];
|
||||
if (self.softLogoutCredentials)
|
||||
{
|
||||
parameters.softLogoutCredentials = self.softLogoutCredentials;
|
||||
self.softLogoutCredentials = nil;
|
||||
}
|
||||
MXLogDebug(@"[MasterTabBarController] presentOnboardingFlow");
|
||||
|
||||
MXWeakify(self);
|
||||
OnboardingCoordinatorBridgePresenter *onboardingCoordinatorBridgePresenter = [[OnboardingCoordinatorBridgePresenter alloc] initWith:parameters];
|
||||
OnboardingCoordinatorBridgePresenter *onboardingCoordinatorBridgePresenter = [[OnboardingCoordinatorBridgePresenter alloc] init];
|
||||
onboardingCoordinatorBridgePresenter.completion = ^{
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self.onboardingCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
||||
|
@ -522,8 +515,8 @@
|
|||
|
||||
- (void)showOnboardingFlow
|
||||
{
|
||||
MXLogDebug(@"[MasterTabBarController] showAuthenticationScreen");
|
||||
|
||||
MXLogDebug(@"[MasterTabBarController] showOnboardingFlow");
|
||||
|
||||
// Check whether an authentication screen is not already shown or preparing
|
||||
if (!self.onboardingCoordinatorBridgePresenter && !self.isOnboardingCoordinatorPreparing)
|
||||
{
|
||||
|
@ -543,7 +536,7 @@
|
|||
{
|
||||
MXLogDebug(@"[MasterTabBarController] showAuthenticationScreenAfterSoftLogout");
|
||||
|
||||
self.softLogoutCredentials = credentials;
|
||||
AuthenticationService.shared.softLogoutCredentials = credentials;
|
||||
|
||||
// Check whether an authentication screen is not already shown or preparing
|
||||
if (!self.onboardingCoordinatorBridgePresenter && !self.isOnboardingCoordinatorPreparing)
|
||||
|
|
|
@ -125,16 +125,7 @@ class AuthenticationService: NSObject {
|
|||
}
|
||||
|
||||
/// Credentials to be used when authenticating after soft logout, otherwise `nil`.
|
||||
var softLogoutCredentials: MXCredentials? {
|
||||
guard MXKAccountManager.shared().activeAccounts.isEmpty else { return nil }
|
||||
for account in MXKAccountManager.shared().accounts {
|
||||
if account.isSoftLogout {
|
||||
return account.mxCredentials
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
var softLogoutCredentials: MXCredentials?
|
||||
|
||||
/// Get the last authenticated [Session], if there is an active session.
|
||||
/// - Returns: The last active session if any, or `nil`
|
||||
|
@ -186,6 +177,7 @@ class AuthenticationService: NSObject {
|
|||
func reset() {
|
||||
loginWizard = nil
|
||||
registrationWizard = nil
|
||||
softLogoutCredentials = nil
|
||||
|
||||
// The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway.
|
||||
let address = state.homeserver.addressFromUser ?? state.homeserver.address
|
||||
|
|
|
@ -53,8 +53,13 @@ class LoginWizard {
|
|||
/// - password: The password of the account.
|
||||
/// - initialDeviceName: The initial device name.
|
||||
/// - deviceID: The device ID, optional. If not provided or nil, the server will generate one.
|
||||
/// - removeOtherAccounts: If set to true, existing accounts with different user identifiers will be removed.
|
||||
/// - Returns: An `MXSession` if the login is successful.
|
||||
func login(login: String, password: String, initialDeviceName: String, deviceID: String? = nil) async throws -> MXSession {
|
||||
func login(login: String,
|
||||
password: String,
|
||||
initialDeviceName: String,
|
||||
deviceID: String? = nil,
|
||||
removeOtherAccounts: Bool = false) async throws -> MXSession {
|
||||
let parameters: LoginPasswordParameters
|
||||
|
||||
if MXTools.isEmailAddress(login) {
|
||||
|
@ -70,16 +75,22 @@ class LoginWizard {
|
|||
}
|
||||
|
||||
let credentials = try await client.login(parameters: parameters)
|
||||
return sessionCreator.createSession(credentials: credentials, client: client)
|
||||
return sessionCreator.createSession(credentials: credentials,
|
||||
client: client,
|
||||
removeOtherAccounts: removeOtherAccounts)
|
||||
}
|
||||
|
||||
|
||||
/// Exchange a login token to an access token.
|
||||
/// - Parameter loginToken: A login token, obtained when login has happened in a WebView, using SSO.
|
||||
/// - Parameters:
|
||||
/// - token: A login token, obtained when login has happened in a WebView, using SSO.
|
||||
/// - removeOtherAccounts: If set to true, existing accounts with different user identifiers will be removed.
|
||||
/// - Returns: An `MXSession` if the login is successful.
|
||||
func login(with token: String) async throws -> MXSession {
|
||||
func login(with token: String, removeOtherAccounts: Bool = false) async throws -> MXSession {
|
||||
let parameters = LoginTokenParameters(token: token)
|
||||
let credentials = try await client.login(parameters: parameters)
|
||||
return sessionCreator.createSession(credentials: credentials, client: client)
|
||||
return sessionCreator.createSession(credentials: credentials,
|
||||
client: client,
|
||||
removeOtherAccounts: removeOtherAccounts)
|
||||
}
|
||||
|
||||
// /// Login to the homeserver by sending a custom JsonDict.
|
||||
|
|
|
@ -255,7 +255,7 @@ class RegistrationWizard {
|
|||
do {
|
||||
let response = try await client.register(parameters: parameters)
|
||||
let credentials = MXCredentials(loginResponse: response, andDefaultCredentials: client.credentials)
|
||||
return .success(sessionCreator.createSession(credentials: credentials, client: client))
|
||||
return .success(sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false))
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
|
||||
|
|
|
@ -21,8 +21,9 @@ protocol SessionCreatorProtocol {
|
|||
/// - Parameters:
|
||||
/// - credentials: The `MXCredentials` for the account.
|
||||
/// - client: The client that completed the authentication.
|
||||
/// - removeOtherAccounts: Flag to remove other accounts than the account specified with the `credentials.userId`.
|
||||
/// - Returns: A new `MXSession` for the account.
|
||||
func createSession(credentials: MXCredentials, client: AuthenticationRestClient) -> MXSession
|
||||
func createSession(credentials: MXCredentials, client: AuthenticationRestClient, removeOtherAccounts: Bool) -> MXSession
|
||||
}
|
||||
|
||||
/// A struct that provides common functionality to create a new session.
|
||||
|
@ -34,19 +35,32 @@ struct SessionCreator: SessionCreatorProtocol {
|
|||
self.accountManager = accountManager
|
||||
}
|
||||
|
||||
func createSession(credentials: MXCredentials, client: AuthenticationRestClient) -> MXSession {
|
||||
// Report the new account in account manager
|
||||
func createSession(credentials: MXCredentials, client: AuthenticationRestClient, removeOtherAccounts: Bool) -> MXSession {
|
||||
// Use identity server provided in the client
|
||||
if credentials.identityServer == nil {
|
||||
credentials.identityServer = client.identityServer
|
||||
}
|
||||
|
||||
let account = MXKAccount(credentials: credentials)
|
||||
|
||||
if let identityServer = credentials.identityServer {
|
||||
account.identityServerURL = identityServer
|
||||
|
||||
if removeOtherAccounts {
|
||||
let otherAccounts = accountManager.accounts.filter({ $0.mxCredentials.userId != credentials.userId })
|
||||
for account in otherAccounts {
|
||||
accountManager.removeAccount(account, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
if let account = accountManager.account(forUserId: credentials.userId) {
|
||||
accountManager.hydrateAccount(account, with: credentials)
|
||||
return account.mxSession
|
||||
} else {
|
||||
let account = MXKAccount(credentials: credentials)
|
||||
|
||||
// set identity server of the new account
|
||||
if let identityServer = credentials.identityServer {
|
||||
account.identityServerURL = identityServer
|
||||
}
|
||||
|
||||
accountManager.addAccount(account, andOpenSession: true)
|
||||
return account.mxSession
|
||||
}
|
||||
|
||||
accountManager.addAccount(account, andOpenSession: true)
|
||||
return account.mxSession
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// Copyright 2021 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 AuthenticationSoftLogoutViewModelResult {
|
||||
/// Login with password
|
||||
case login(String)
|
||||
/// Forgot password
|
||||
case forgotPassword
|
||||
/// Clear all user data
|
||||
case clearAllData
|
||||
/// Continue using the supplied SSO provider.
|
||||
case continueWithSSO(SSOIdentityProvider)
|
||||
/// Continue using the fallback page
|
||||
case fallback
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct AuthenticationSoftLogoutViewState: BindableState {
|
||||
/// Soft logout credentials
|
||||
var credentials: SoftLogoutCredentials
|
||||
|
||||
/// Data about the selected homeserver.
|
||||
var homeserver: AuthenticationHomeserverViewData
|
||||
|
||||
/// 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: AuthenticationSoftLogoutBindings
|
||||
|
||||
/// Whether to show login form.
|
||||
var showLoginForm: Bool {
|
||||
homeserver.showLoginForm
|
||||
}
|
||||
|
||||
/// Whether to show any SSO buttons.
|
||||
var showSSOButtons: Bool {
|
||||
!homeserver.ssoIdentityProviders.isEmpty
|
||||
}
|
||||
|
||||
/// Whether to show recover encryption keys message
|
||||
var showRecoverEncryptionKeysMessage: Bool {
|
||||
keyBackupNeeded
|
||||
}
|
||||
|
||||
/// Whether the password is valid and the user can continue.
|
||||
var hasInvalidPassword: Bool {
|
||||
bindings.password.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationSoftLogoutBindings {
|
||||
/// The password input by the user.
|
||||
var password: String
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<AuthenticationSoftLogoutErrorType>?
|
||||
}
|
||||
|
||||
enum AuthenticationSoftLogoutViewAction {
|
||||
/// Login.
|
||||
case login
|
||||
/// Forgot password
|
||||
case forgotPassword
|
||||
/// Clear all user data.
|
||||
case clearAllData
|
||||
/// Continue using the supplied SSO provider.
|
||||
case continueWithSSO(SSOIdentityProvider)
|
||||
/// Continue using the fallback page
|
||||
case fallback
|
||||
}
|
||||
|
||||
enum AuthenticationSoftLogoutErrorType: Hashable {
|
||||
/// An error response from the homeserver.
|
||||
case mxError(String)
|
||||
/// An unknown error occurred.
|
||||
case unknown
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// Copyright 2021 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 AuthenticationSoftLogoutViewModelType = StateStoreViewModel<AuthenticationSoftLogoutViewState,
|
||||
Never,
|
||||
AuthenticationSoftLogoutViewAction>
|
||||
class AuthenticationSoftLogoutViewModel: AuthenticationSoftLogoutViewModelType, AuthenticationSoftLogoutViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: (@MainActor (AuthenticationSoftLogoutViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(credentials: SoftLogoutCredentials,
|
||||
homeserver: AuthenticationHomeserverViewData,
|
||||
keyBackupNeeded: Bool,
|
||||
password: String = "") {
|
||||
let bindings = AuthenticationSoftLogoutBindings(password: password)
|
||||
let viewState = AuthenticationSoftLogoutViewState(credentials: credentials,
|
||||
homeserver: homeserver,
|
||||
keyBackupNeeded: keyBackupNeeded,
|
||||
bindings: bindings)
|
||||
super.init(initialViewState: viewState)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: AuthenticationSoftLogoutViewAction) {
|
||||
switch viewAction {
|
||||
case .login:
|
||||
Task { await callback?(.login(state.bindings.password)) }
|
||||
case .forgotPassword:
|
||||
Task { await callback?(.forgotPassword) }
|
||||
case .clearAllData:
|
||||
Task { await callback?(.clearAllData) }
|
||||
case .continueWithSSO(let provider):
|
||||
Task { await callback?(.continueWithSSO(provider)) }
|
||||
case .fallback:
|
||||
Task { await callback?(.fallback) }
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor func displayError(_ type: AuthenticationSoftLogoutErrorType) {
|
||||
switch type {
|
||||
case .mxError(let message):
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
title: VectorL10n.error,
|
||||
message: message)
|
||||
case .unknown:
|
||||
state.bindings.alertInfo = AlertInfo(id: type)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright 2021 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 AuthenticationSoftLogoutViewModelProtocol {
|
||||
|
||||
var callback: (@MainActor (AuthenticationSoftLogoutViewModelResult) -> Void)? { get set }
|
||||
var context: AuthenticationSoftLogoutViewModelType.Context { get }
|
||||
|
||||
/// Display an error to the user.
|
||||
@MainActor func displayError(_ type: AuthenticationSoftLogoutErrorType)
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
//
|
||||
// Copyright 2021 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
|
||||
import CommonKit
|
||||
|
||||
struct AuthenticationSoftLogoutCoordinatorParameters {
|
||||
let navigationRouter: NavigationRouterType
|
||||
let authenticationService: AuthenticationService
|
||||
let credentials: SoftLogoutCredentials
|
||||
let keyBackupNeeded: Bool
|
||||
}
|
||||
|
||||
enum AuthenticationSoftLogoutCoordinatorResult {
|
||||
/// Login was successful with the associated session created.
|
||||
case success(session: MXSession, password: String)
|
||||
/// Clear all user data
|
||||
case clearAllData
|
||||
/// Continue using the supplied SSO provider.
|
||||
case continueWithSSO(SSOIdentityProvider)
|
||||
/// Continue using the fallback page
|
||||
case fallback
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class AuthenticationSoftLogoutCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: AuthenticationSoftLogoutCoordinatorParameters
|
||||
private let authenticationSoftLogoutHostingController: VectorHostingController
|
||||
private var authenticationSoftLogoutViewModel: AuthenticationSoftLogoutViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var successIndicator: UserIndicator?
|
||||
|
||||
/// The wizard used to handle the registration flow.
|
||||
private var loginWizard: LoginWizard? { parameters.authenticationService.loginWizard }
|
||||
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
|
||||
private var currentTask: Task<Void, Error>? {
|
||||
willSet {
|
||||
currentTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (AuthenticationSoftLogoutCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: AuthenticationSoftLogoutCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let homeserver = parameters.authenticationService.state.homeserver
|
||||
|
||||
let viewModel = AuthenticationSoftLogoutViewModel(credentials: parameters.credentials,
|
||||
homeserver: homeserver.viewData,
|
||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||
let view = AuthenticationSoftLogoutScreen(viewModel: viewModel.context)
|
||||
authenticationSoftLogoutViewModel = viewModel
|
||||
authenticationSoftLogoutHostingController = VectorHostingController(rootView: view)
|
||||
authenticationSoftLogoutHostingController.vc_removeBackTitle()
|
||||
authenticationSoftLogoutHostingController.enableNavigationBarScrollEdgeAppearance = true
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: authenticationSoftLogoutHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[AuthenticationSoftLogoutCoordinator] did start.")
|
||||
Task { await setupViewModel() }
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.authenticationSoftLogoutHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Set up the view model. This method is extracted from `start()` so it can run on the `MainActor`.
|
||||
@MainActor private func setupViewModel() {
|
||||
authenticationSoftLogoutViewModel.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[AuthenticationSoftLogoutCoordinator] AuthenticationSoftLogoutViewModel 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 .continueWithSSO(let provider):
|
||||
self.callback?(.continueWithSSO(provider))
|
||||
case .fallback:
|
||||
self.callback?(.fallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
@MainActor private func startLoading() {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.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("[AuthenticationSoftLogoutCoordinator] showForgotPasswordScreen")
|
||||
|
||||
guard let loginWizard = loginWizard else {
|
||||
MXLog.failure("[AuthenticationSoftLogoutCoordinator] The login wizard was requested before getting the login flow.")
|
||||
return
|
||||
}
|
||||
|
||||
let modalRouter = NavigationRouter()
|
||||
|
||||
let parameters = AuthenticationForgotPasswordCoordinatorParameters(navigationRouter: modalRouter,
|
||||
loginWizard: loginWizard)
|
||||
let coordinator = AuthenticationForgotPasswordCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
switch result {
|
||||
case .success:
|
||||
self.navigationRouter.dismissModule(animated: true, completion: nil)
|
||||
self.successIndicator = self.indicatorPresenter.present(.success(label: VectorL10n.done))
|
||||
case .cancel:
|
||||
self.navigationRouter.dismissModule(animated: true, completion: nil)
|
||||
}
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
modalRouter.setRootModule(coordinator)
|
||||
|
||||
navigationRouter.present(modalRouter, animated: true)
|
||||
}
|
||||
|
||||
/// Login with the supplied username and password.
|
||||
@MainActor private func login(withPassword password: String) {
|
||||
guard let loginWizard = loginWizard else {
|
||||
MXLog.failure("[AuthenticationSoftLogoutCoordinator] The login wizard was requested before getting the login flow.")
|
||||
return
|
||||
}
|
||||
|
||||
let userId = parameters.credentials.userId
|
||||
let deviceId = parameters.credentials.deviceId
|
||||
startLoading()
|
||||
|
||||
currentTask = Task { [weak self] in
|
||||
do {
|
||||
let session = try await loginWizard.login(login: userId,
|
||||
password: password,
|
||||
initialDeviceName: UIDevice.current.initialDisplayName,
|
||||
deviceID: deviceId,
|
||||
removeOtherAccounts: true)
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
callback?(.success(session: session, password: password))
|
||||
|
||||
self?.stopLoading()
|
||||
} catch {
|
||||
self?.stopLoading()
|
||||
self?.handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
@MainActor private func handleError(_ error: Error) {
|
||||
if let mxError = MXError(nsError: error as NSError) {
|
||||
authenticationSoftLogoutViewModel.displayError(.mxError(mxError.error))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Handle another other error types as needed.
|
||||
|
||||
authenticationSoftLogoutViewModel.displayError(.unknown)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// Copyright 2021 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.
|
||||
@available(iOS 14.0, *)
|
||||
enum MockAuthenticationSoftLogoutScreenState: MockScreenState, 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 ssoOnly
|
||||
case noSSO
|
||||
case fallback
|
||||
case noKeyBackup
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
AuthenticationSoftLogoutScreen.self
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: AuthenticationSoftLogoutViewModel
|
||||
let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org",
|
||||
homeserverName: "matrix.org",
|
||||
userDisplayName: "mock",
|
||||
deviceId: nil)
|
||||
switch self {
|
||||
case .emptyPassword:
|
||||
viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockMatrixDotOrg,
|
||||
keyBackupNeeded: true)
|
||||
case .enteredPassword:
|
||||
viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockMatrixDotOrg,
|
||||
keyBackupNeeded: true,
|
||||
password: "12345678")
|
||||
case .ssoOnly:
|
||||
viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockEnterpriseSSO,
|
||||
keyBackupNeeded: true)
|
||||
case .noSSO:
|
||||
viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockBasicServer,
|
||||
keyBackupNeeded: true)
|
||||
case .fallback:
|
||||
viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockFallback,
|
||||
keyBackupNeeded: true)
|
||||
case .noKeyBackup:
|
||||
viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockFallback,
|
||||
keyBackupNeeded: false)
|
||||
}
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[viewModel], AnyView(AuthenticationSoftLogoutScreen(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationSoftLogoutScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationSoftLogoutUITests(selector: #selector(verifyAuthenticationSoftLogoutScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationSoftLogoutScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationSoftLogoutScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyPassword:
|
||||
verifyEmptyPassword()
|
||||
case .enteredPassword:
|
||||
verifyEnteredPassword()
|
||||
case .ssoOnly:
|
||||
verifySSOOnly()
|
||||
case .noSSO:
|
||||
verifyNoSSO()
|
||||
case .fallback:
|
||||
verifyFallback()
|
||||
case .noKeyBackup:
|
||||
verifyNoKeyBackup()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyPassword() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataTitleLabel"].exists, "The clear data title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage1Label"].exists, "The clear data message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage2Label"].exists, "The clear data message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["orLabel"].exists, "The or label for SSO should be shown.")
|
||||
|
||||
let passwordTextField = app.secureTextFields["passwordTextField"]
|
||||
XCTAssertTrue(passwordTextField.exists, "The password text field should be shown.")
|
||||
XCTAssertEqual(passwordTextField.label, "Password", "The password text field should be showing the placeholder before text is input.")
|
||||
|
||||
let loginButton = app.buttons["loginButton"]
|
||||
XCTAssertTrue(loginButton.exists, "The login button should be shown.")
|
||||
XCTAssertFalse(loginButton.isEnabled, "The login button should be disabled before text is input.")
|
||||
|
||||
let forgotPasswordButton = app.buttons["forgotPasswordButton"]
|
||||
XCTAssertTrue(forgotPasswordButton.exists, "The forgot password button should be shown.")
|
||||
XCTAssertTrue(forgotPasswordButton.isEnabled, "The forgot password button should be enabled.")
|
||||
|
||||
let fallbackButton = app.buttons["fallbackButton"]
|
||||
XCTAssertFalse(fallbackButton.exists, "The fallback button should not be shown.")
|
||||
|
||||
let clearDataButton = app.buttons["clearDataButton"]
|
||||
XCTAssertTrue(clearDataButton.exists, "The clear data button should be shown.")
|
||||
XCTAssertTrue(clearDataButton.isEnabled, "The clear data button should be enabled.")
|
||||
|
||||
let ssoButtons = app.buttons.matching(identifier: "ssoButton")
|
||||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyEnteredPassword() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataTitleLabel"].exists, "The clear data title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage1Label"].exists, "The clear data message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage2Label"].exists, "The clear data message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["orLabel"].exists, "The or label for SSO should be shown.")
|
||||
|
||||
let passwordTextField = app.secureTextFields["passwordTextField"]
|
||||
XCTAssertTrue(passwordTextField.exists, "The password text field should be shown.")
|
||||
XCTAssertEqual(passwordTextField.value as? String, "••••••••", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let loginButton = app.buttons["loginButton"]
|
||||
XCTAssertTrue(loginButton.exists, "The login button should be shown.")
|
||||
XCTAssertTrue(loginButton.isEnabled, "The login button should be enabled after text is input.")
|
||||
|
||||
let forgotPasswordButton = app.buttons["forgotPasswordButton"]
|
||||
XCTAssertTrue(forgotPasswordButton.exists, "The forgot password button should be shown.")
|
||||
XCTAssertTrue(forgotPasswordButton.isEnabled, "The forgot password button should be enabled.")
|
||||
|
||||
let fallbackButton = app.buttons["fallbackButton"]
|
||||
XCTAssertFalse(fallbackButton.exists, "The fallback button should not be shown.")
|
||||
|
||||
let clearDataButton = app.buttons["clearDataButton"]
|
||||
XCTAssertTrue(clearDataButton.exists, "The clear data button should be shown.")
|
||||
XCTAssertTrue(clearDataButton.isEnabled, "The clear data button should be enabled.")
|
||||
|
||||
let ssoButtons = app.buttons.matching(identifier: "ssoButton")
|
||||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifySSOOnly() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataTitleLabel"].exists, "The clear data title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage1Label"].exists, "The clear data message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage2Label"].exists, "The clear data message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["orLabel"].exists, "The or label for SSO should be shown.")
|
||||
|
||||
let passwordTextField = app.secureTextFields["passwordTextField"]
|
||||
XCTAssertFalse(passwordTextField.exists, "The password text field should not be shown.")
|
||||
|
||||
let loginButton = app.buttons["loginButton"]
|
||||
XCTAssertFalse(loginButton.exists, "The login button should not be shown.")
|
||||
|
||||
let forgotPasswordButton = app.buttons["forgotPasswordButton"]
|
||||
XCTAssertFalse(forgotPasswordButton.exists, "The forgot password button should not be shown.")
|
||||
|
||||
let fallbackButton = app.buttons["fallbackButton"]
|
||||
XCTAssertFalse(fallbackButton.exists, "The fallback button should not be shown.")
|
||||
|
||||
let clearDataButton = app.buttons["clearDataButton"]
|
||||
XCTAssertTrue(clearDataButton.exists, "The clear data button should be shown.")
|
||||
XCTAssertTrue(clearDataButton.isEnabled, "The clear data button should be enabled.")
|
||||
|
||||
let ssoButtons = app.buttons.matching(identifier: "ssoButton")
|
||||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyNoSSO() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataTitleLabel"].exists, "The clear data title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage1Label"].exists, "The clear data message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage2Label"].exists, "The clear data message 2 should be shown.")
|
||||
XCTAssertFalse(app.staticTexts["orLabel"].exists, "The or label for SSO should not be shown.")
|
||||
|
||||
let passwordTextField = app.secureTextFields["passwordTextField"]
|
||||
XCTAssertTrue(passwordTextField.exists, "The password text field should be shown.")
|
||||
|
||||
let loginButton = app.buttons["loginButton"]
|
||||
XCTAssertTrue(loginButton.exists, "The login button should be shown.")
|
||||
|
||||
let forgotPasswordButton = app.buttons["forgotPasswordButton"]
|
||||
XCTAssertTrue(forgotPasswordButton.exists, "The forgot password button should be shown.")
|
||||
|
||||
let fallbackButton = app.buttons["fallbackButton"]
|
||||
XCTAssertFalse(fallbackButton.exists, "The fallback button should not be shown.")
|
||||
|
||||
let clearDataButton = app.buttons["clearDataButton"]
|
||||
XCTAssertTrue(clearDataButton.exists, "The clear data button should be shown.")
|
||||
XCTAssertTrue(clearDataButton.isEnabled, "The clear data button should be enabled.")
|
||||
|
||||
let ssoButtons = app.buttons.matching(identifier: "ssoButton")
|
||||
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyFallback() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataTitleLabel"].exists, "The clear data title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage1Label"].exists, "The clear data message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["clearDataMessage2Label"].exists, "The clear data message 2 should be shown.")
|
||||
XCTAssertFalse(app.staticTexts["orLabel"].exists, "The or label for SSO should not be shown.")
|
||||
|
||||
let passwordTextField = app.secureTextFields["passwordTextField"]
|
||||
XCTAssertFalse(passwordTextField.exists, "The password text field should not be shown.")
|
||||
|
||||
let loginButton = app.buttons["loginButton"]
|
||||
XCTAssertFalse(loginButton.exists, "The login button should not be shown.")
|
||||
|
||||
let forgotPasswordButton = app.buttons["forgotPasswordButton"]
|
||||
XCTAssertFalse(forgotPasswordButton.exists, "The forgot password button should not be shown.")
|
||||
|
||||
let fallbackButton = app.buttons["fallbackButton"]
|
||||
XCTAssertTrue(fallbackButton.exists, "The fallback button should be shown.")
|
||||
XCTAssertTrue(fallbackButton.isEnabled, "The fallback button should be enabled.")
|
||||
|
||||
let clearDataButton = app.buttons["clearDataButton"]
|
||||
XCTAssertTrue(clearDataButton.exists, "The clear data button should be shown.")
|
||||
XCTAssertTrue(clearDataButton.isEnabled, "The clear data button should be enabled.")
|
||||
|
||||
let ssoButtons = app.buttons.matching(identifier: "ssoButton")
|
||||
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyNoKeyBackup() {
|
||||
XCTAssertFalse(app.staticTexts["messageLabel2"].exists, "The message 2 should not be shown.")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class AuthenticationSoftLogoutViewModelTests: XCTestCase {
|
||||
|
||||
@MainActor func testInitialStateForMatrixOrg() async {
|
||||
let credentials = SoftLogoutCredentials(userId: "mock_user_id",
|
||||
homeserverName: "https://matrix.org",
|
||||
userDisplayName: "mock_username",
|
||||
deviceId: nil)
|
||||
let viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockMatrixDotOrg,
|
||||
keyBackupNeeded: true)
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a view model where the user hasn't yet sent the verification email.
|
||||
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
XCTAssert(context.viewState.hasInvalidPassword, "The view model should start with an invalid password.")
|
||||
XCTAssert(context.viewState.showSSOButtons, "The view model should show SSO buttons for the given homeserver.")
|
||||
XCTAssert(context.viewState.showLoginForm, "The view model should show login form for the given homeserver.")
|
||||
XCTAssert(context.viewState.showRecoverEncryptionKeysMessage, "The view model should show recover encryption keys message.")
|
||||
}
|
||||
|
||||
@MainActor func testInitialStateForNoSSO() async {
|
||||
let credentials = SoftLogoutCredentials(userId: "mock_user_id",
|
||||
homeserverName: "https://example.com",
|
||||
userDisplayName: "mock_username",
|
||||
deviceId: nil)
|
||||
let viewModel = AuthenticationSoftLogoutViewModel(credentials: credentials,
|
||||
homeserver: .mockBasicServer,
|
||||
keyBackupNeeded: false)
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a view model where the user hasn't yet sent the verification email.
|
||||
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
XCTAssert(context.viewState.hasInvalidPassword, "The view model should start with an invalid password.")
|
||||
XCTAssertFalse(context.viewState.showSSOButtons, "The view model should not show SSO buttons for the given homeserver.")
|
||||
XCTAssert(context.viewState.showLoginForm, "The view model should show login form for the given homeserver.")
|
||||
XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
//
|
||||
// Copyright 2021 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 AuthenticationSoftLogoutScreen: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: AuthenticationSoftLogoutViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 36) {
|
||||
header
|
||||
if viewModel.viewState.showLoginForm {
|
||||
loginForm
|
||||
} else if !viewModel.viewState.showSSOButtons {
|
||||
fallbackButton
|
||||
}
|
||||
clearDataForm
|
||||
if viewModel.viewState.showSSOButtons {
|
||||
Text(VectorL10n.or)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.accessibilityIdentifier("orLabel")
|
||||
ssoButtons
|
||||
}
|
||||
}
|
||||
.readableFrame()
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.alert(item: $viewModel.alertInfo) { $0.alert }
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
|
||||
/// The title, message and icon at the top of the screen.
|
||||
var header: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
OnboardingIconImage(image: Asset.Images.authenticationPasswordIcon)
|
||||
.padding(.bottom, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Text(VectorL10n.authSoftlogoutSignIn)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
Text(VectorL10n.authSoftlogoutReason(viewModel.viewState.credentials.homeserverName, viewModel.viewState.credentials.userDisplayName, viewModel.viewState.credentials.userId))
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("messageLabel1")
|
||||
|
||||
if viewModel.viewState.showRecoverEncryptionKeysMessage {
|
||||
Text(VectorL10n.authSoftlogoutRecoverEncryptionKeys)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("messageLabel2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The text field and submit button where the user enters an email address.
|
||||
var loginForm: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
passwordTextField
|
||||
|
||||
Button(action: forgotPassword) {
|
||||
Text(VectorL10n.authenticationLoginForgotPassword)
|
||||
.font(theme.fonts.body)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.bottom, 8)
|
||||
.accessibilityIdentifier("forgotPasswordButton")
|
||||
|
||||
Button(action: login) {
|
||||
Text(VectorL10n.login)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(viewModel.viewState.hasInvalidPassword)
|
||||
.accessibilityIdentifier("loginButton")
|
||||
}
|
||||
}
|
||||
|
||||
/// A fallback button that can be used for login.
|
||||
var fallbackButton: some View {
|
||||
Button(action: fallback) {
|
||||
Text(VectorL10n.login)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.accessibilityIdentifier("fallbackButton")
|
||||
}
|
||||
|
||||
/// The text field and submit button where the user enters an email address.
|
||||
var clearDataForm: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(VectorL10n.authSoftlogoutClearData)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("clearDataTitleLabel")
|
||||
|
||||
Text(VectorL10n.authSoftlogoutClearDataMessage1)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("clearDataMessage1Label")
|
||||
|
||||
Text(VectorL10n.authSoftlogoutClearDataMessage2)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("clearDataMessage2Label")
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Button(action: clearData) {
|
||||
Text(VectorL10n.authSoftlogoutClearDataButton)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle(customColor: theme.colors.alert))
|
||||
.accessibilityIdentifier("clearDataButton")
|
||||
}
|
||||
}
|
||||
|
||||
/// The text field, extracted for iOS 15 modifiers to be applied.
|
||||
var passwordTextField: some View {
|
||||
RoundedBorderTextField(placeHolder: VectorL10n.loginPasswordPlaceholder,
|
||||
text: $viewModel.password,
|
||||
configuration: UIKitTextInputConfiguration(returnKeyType: .done,
|
||||
isSecureTextEntry: true),
|
||||
onCommit: login)
|
||||
.accessibilityIdentifier("passwordTextField")
|
||||
}
|
||||
|
||||
/// A list of SSO buttons that can be used for login.
|
||||
var ssoButtons: some View {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(viewModel.viewState.homeserver.ssoIdentityProviders) { provider in
|
||||
AuthenticationSSOButton(provider: provider) {
|
||||
viewModel.send(viewAction: .continueWithSSO(provider))
|
||||
}
|
||||
.accessibilityIdentifier("ssoButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the `login` view action so long as a valid email address has been input.
|
||||
func login() {
|
||||
guard !viewModel.viewState.hasInvalidPassword else { return }
|
||||
viewModel.send(viewAction: .login)
|
||||
}
|
||||
|
||||
/// Sends the `fallback` view action.
|
||||
func fallback() {
|
||||
viewModel.send(viewAction: .fallback)
|
||||
}
|
||||
|
||||
/// Sends the `forgotPassword` view action.
|
||||
func forgotPassword() {
|
||||
viewModel.send(viewAction: .forgotPassword)
|
||||
}
|
||||
|
||||
/// Sends the `clearAllData` view action.
|
||||
func clearData() {
|
||||
viewModel.send(viewAction: .clearAllData)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AuthenticationSoftLogoutScreen_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockAuthenticationSoftLogoutScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup(addNavigation: true)
|
||||
.navigationViewStyle(.stack)
|
||||
stateRenderer.screenGroup(addNavigation: true)
|
||||
.navigationViewStyle(.stack)
|
||||
.theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ enum MockAppScreens {
|
|||
MockAuthenticationServerSelectionScreenState.self,
|
||||
MockAuthenticationForgotPasswordScreenState.self,
|
||||
MockAuthenticationChoosePasswordScreenState.self,
|
||||
MockAuthenticationSoftLogoutScreenState.self,
|
||||
MockOnboardingCelebrationScreenState.self,
|
||||
MockOnboardingAvatarScreenState.self,
|
||||
MockOnboardingDisplayNameScreenState.self,
|
||||
|
|
|
@ -20,7 +20,7 @@ import Foundation
|
|||
|
||||
struct MockSessionCreator: SessionCreatorProtocol {
|
||||
/// Returns a basic session created from the supplied credentials. This prevents the app from setting up the account during tests.
|
||||
func createSession(credentials: MXCredentials, client: AuthenticationRestClient) -> MXSession {
|
||||
func createSession(credentials: MXCredentials, client: AuthenticationRestClient, removeOtherAccounts: Bool) -> MXSession {
|
||||
let client = MXRestClient(credentials: credentials,
|
||||
unauthenticatedHandler: { _,_,_,_ in }) // The handler is expected if credentials are set.
|
||||
return MXSession(matrixRestClient: client)
|
||||
|
|
|
@ -29,7 +29,7 @@ class SessionCreatorTests: XCTestCase {
|
|||
accessToken: "mock_access_token")
|
||||
let client = MXRestClient(credentials: credentials)
|
||||
client.identityServer = mockIS
|
||||
let session = sessionCreator.createSession(credentials: credentials, client: client)
|
||||
let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false)
|
||||
|
||||
XCTAssertEqual(credentials.identityServer, mockIS)
|
||||
XCTAssertEqual(session.credentials.identityServer, mockIS)
|
||||
|
|
1
changelog.d/4685.bugfix
Normal file
1
changelog.d/4685.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Settings: Allow account deactivation when the account was created using SSO.
|
1
changelog.d/6181.feature
Normal file
1
changelog.d/6181.feature
Normal file
|
@ -0,0 +1 @@
|
|||
FTUE: Implement soft logout screen.
|
Loading…
Reference in a new issue