Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FTUE: Soft logout #6257

Merged
merged 15 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Riot/Categories/MXFileStore.swift
Original file line number Diff line number Diff line change
@@ -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? {
ismailgulek marked this conversation as resolved.
Show resolved Hide resolved
await withCheckedContinuation({ continuation in
asyncUsers(withUserIds: [userId]) { users in
continuation.resume(returning: users.first?.displayname)
}
})
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ struct LegacyAuthenticationCoordinatorParameters {
let navigationRouter: NavigationRouterType
/// Whether or not the coordinator should show the loading spinner, key verification etc.
let canPresentAdditionalScreens: Bool
/// The credentials to use if a soft logout has taken place.
let softLogoutCredentials: MXCredentials?
}

/// A coordinator that handles authentication, verification and setting a PIN using the old UIViewController flow for iOS 12 & 13.
Expand Down Expand Up @@ -61,6 +63,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens

let authenticationViewController = AuthenticationViewController()
authenticationViewController.softLogoutCredentials = parameters.softLogoutCredentials
self.authenticationViewController = authenticationViewController

// Preload the view as this can a second and lock up the UI at presentation.
Expand All @@ -87,10 +90,6 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
authenticationViewController.authType = authenticationFlow.mxkType
}

func update(softLogoutCredentials: MXCredentials) {
authenticationViewController.softLogoutCredentials = softLogoutCredentials
}

func presentPendingScreensIfNecessary() {
canPresentAdditionalScreens = true

Expand Down
99 changes: 94 additions & 5 deletions Riot/Modules/Onboarding/AuthenticationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ struct AuthenticationCoordinatorParameters {
let initialScreen: AuthenticationCoordinator.EntryPoint
/// Whether or not the coordinator should show the loading spinner, key verification etc.
let canPresentAdditionalScreens: Bool
/// Soft logout credentials
let softLogoutCredentials: MXCredentials?
}

/// A coordinator that handles authentication, verification and setting a PIN.
Expand Down Expand Up @@ -55,6 +57,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc

/// Whether the coordinator can present further screens after a successful login has occurred.
private var canPresentAdditionalScreens: Bool
/// Soft logout credentials
private let softLogoutCredentials: MXCredentials?
/// `true` if presentation of the verification screen is blocked by `canPresentAdditionalScreens`.
private var isWaitingToPresentCompleteSecurity = false

Expand All @@ -81,6 +85,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
self.navigationRouter = parameters.navigationRouter
self.initialScreen = parameters.initialScreen
self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens
self.softLogoutCredentials = parameters.softLogoutCredentials

indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.navigationRouter.toPresentable())

Expand Down Expand Up @@ -116,6 +121,20 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc

/// Starts the authentication flow.
@MainActor private func startAuthenticationFlow() async {
if let softLogoutCredentials = 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 {
Expand Down Expand Up @@ -200,6 +219,61 @@ 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.authenticationService.reset()
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)

if navigationRouter.modules.isEmpty {
navigationRouter.setRootModule(coordinator, popCompletion: nil)
} else {
navigationRouter.push(coordinator, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
ismailgulek marked this conversation as resolved.
Show resolved Hide resolved
}

/// Displays the next view in the flow based on the result from the registration screen.
@MainActor private func loginCoordinator(_ coordinator: AuthenticationLoginCoordinator,
Expand Down Expand Up @@ -471,8 +545,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)")

Expand Down Expand Up @@ -672,9 +764,6 @@ extension AuthenticationCoordinator {
// unused
}

func update(softLogoutCredentials: MXCredentials) {
// unused
}
}

// MARK: - AuthFallBackViewControllerDelegate
Expand Down
36 changes: 22 additions & 14 deletions Riot/Modules/Onboarding/OnboardingCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,24 @@ 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,
softLogoutCredentials: parameters.softLogoutCredentials)
legacyAuthenticationCoordinator = LegacyAuthenticationCoordinator(parameters: params)

super.init()
}

// MARK: - Public

func start() {
// TODO: Manage a separate flow for soft logout that just uses AuthenticationCoordinator
if parameters.softLogoutCredentials == nil, BuildSettings.authScreenShowRegister {
if parameters.softLogoutCredentials != nil {
if BuildSettings.onboardingEnableNewAuthenticationFlow {
beginAuthentication(with: .login)
} else {
showLegacyAuthenticationScreen()
}
} else if BuildSettings.authScreenShowRegister {
showSplashScreen()
} else {
showLegacyAuthenticationScreen()
Expand Down Expand Up @@ -191,23 +198,28 @@ 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,
initialScreen: initialScreen,
canPresentAdditionalScreens: false)
canPresentAdditionalScreens: false,
softLogoutCredentials: self.parameters.softLogoutCredentials)
let coordinator = AuthenticationCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }

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.isShowingLegacyAuthentication = false
self.authenticationFinished = false
AppDelegate.theDelegate().logoutSendingRequestServer(true, completion: nil)
ismailgulek marked this conversation as resolved.
Show resolved Hide resolved
case .cancel(let flow):
self.cancelAuthentication(flow: flow)
}
Expand All @@ -217,7 +229,7 @@ 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() {
guard !isShowingLegacyAuthentication else { return }
Expand All @@ -233,18 +245,14 @@ 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ import Foundation
@objcMembers
class OnboardingCoordinatorBridgePresenterParameters: NSObject {
/// The credentials to use after a soft logout has taken place.
var softLogoutCredentials: MXCredentials?
let softLogoutCredentials: MXCredentials?

init(softLogoutCredentials: MXCredentials?) {
self.softLogoutCredentials = softLogoutCredentials

super.init()
}
}

/// OnboardingCoordinatorBridgePresenter enables to start OnboardingCoordinator from a view controller.
Expand Down
8 changes: 2 additions & 6 deletions Riot/Modules/TabBar/MasterTabBarController.m
Original file line number Diff line number Diff line change
Expand Up @@ -475,12 +475,8 @@ - (void)onMatrixSessionStateDidChange:(NSNotification *)notif
// 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;
}
OnboardingCoordinatorBridgePresenterParameters *parameters = [[OnboardingCoordinatorBridgePresenterParameters alloc] initWithSoftLogoutCredentials:self.softLogoutCredentials];
self.softLogoutCredentials = nil;

MXWeakify(self);
OnboardingCoordinatorBridgePresenter *onboardingCoordinatorBridgePresenter = [[OnboardingCoordinatorBridgePresenter alloc] initWith:parameters];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class LoginWizard {
/// - initialDeviceName: The initial device name.
/// - deviceID: The device ID, optional. If not provided or nil, the server will generate one.
/// - 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, resetOthers: Bool = false) async throws -> MXSession {
ismailgulek marked this conversation as resolved.
Show resolved Hide resolved
let parameters: LoginPasswordParameters

if MXTools.isEmailAddress(login) {
Expand All @@ -70,16 +70,16 @@ class LoginWizard {
}

let credentials = try await client.login(parameters: parameters)
return sessionCreator.createSession(credentials: credentials, client: client)
return sessionCreator.createSession(credentials: credentials, client: client, resetOthers: resetOthers)
}

/// Exchange a login token to an access token.
/// - Parameter loginToken: A login token, obtained when login has happened in a WebView, using SSO.
/// - Returns: An `MXSession` if the login is successful.
func login(with token: String) async throws -> MXSession {
func login(with token: String, resetOthers: 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, resetOthers: resetOthers)
}

// /// Login to the homeserver by sending a custom JsonDict.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, resetOthers: false))
} catch {
let nsError = error as NSError

Expand Down
Loading