Merge branch 'develop' into ismail/6175_signout_from_all

This commit is contained in:
ismailgulek 2022-06-09 20:38:34 +03:00
commit 952a84bc98
No known key found for this signature in database
GPG key ID: E96336D42D9470A9
28 changed files with 1499 additions and 151 deletions

View 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)
}
})
}
}

View file

@ -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()

View file

@ -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

View file

@ -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];

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}
}
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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))
)
}
}

View file

@ -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.")
}
}

View file

@ -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.")
}
}

View file

@ -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)
}
}

View file

@ -30,6 +30,7 @@ enum MockAppScreens {
MockAuthenticationServerSelectionScreenState.self,
MockAuthenticationForgotPasswordScreenState.self,
MockAuthenticationChoosePasswordScreenState.self,
MockAuthenticationSoftLogoutScreenState.self,
MockOnboardingCelebrationScreenState.self,
MockOnboardingAvatarScreenState.self,
MockOnboardingDisplayNameScreenState.self,

View file

@ -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)

View file

@ -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
View file

@ -0,0 +1 @@
Settings: Allow account deactivation when the account was created using SSO.

1
changelog.d/6181.feature Normal file
View file

@ -0,0 +1 @@
FTUE: Implement soft logout screen.