diff --git a/CHANGES.md b/CHANGES.md index 3a269699d9..9aacbd1686 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +## Changes in 1.11.3 (2023-09-13) + +🐛 Bugfixes + +- Show OIDC account management UI using embedded browser instead of system browser. ([#7671](https://github.com/vector-im/element-ios/issues/7671)) +- Hide Sign Out X/All Sessions buttons in the Device Manager when using OIDC. ([#7672](https://github.com/vector-im/element-ios/issues/7672)) + + ## Changes in 1.11.2 (2023-09-12) 🙌 Improvements diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index ad10bbd700..eabcc9b01e 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.11.2 -CURRENT_PROJECT_VERSION = 1.11.2 +MARKETING_VERSION = 1.11.3 +CURRENT_PROJECT_VERSION = 1.11.3 diff --git a/Riot/Modules/Authentication/SSO/SSOAccountService.swift b/Riot/Modules/Authentication/SSO/SSOAccountService.swift new file mode 100644 index 0000000000..4623fb84f8 --- /dev/null +++ b/Riot/Modules/Authentication/SSO/SSOAccountService.swift @@ -0,0 +1,48 @@ +// +// Copyright 2020 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 + +@objcMembers +/// A service for the SSOAuthenticationPresenter that allows to open an OIDC account management URL. +/// +/// Both `callBackURLScheme` and `loginToken` are unneeded for this use case and return `nil`. +final class SSOAccountService: NSObject, SSOAuthenticationServiceProtocol { + + // MARK: - Properties + + private let accountURL: URL + + let callBackURLScheme: String? = nil + + // MARK: - Setup + + init(accountURL: URL) { + self.accountURL = accountURL + super.init() + } + + // MARK: - Public + + func authenticationURL(for identityProvider: String?, transactionId: String) -> URL? { + accountURL + } + + func loginToken(from url: URL) -> String? { + MXLog.error("The account service shouldn't receive a completion callback.") + return nil + } +} diff --git a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift index 1145d8651a..b45f3a804b 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift @@ -37,7 +37,7 @@ final class SSOAuthenticationPresenter: NSObject { // MARK: - Properties - private let ssoAuthenticationService: SSOAuthenticationService + private let ssoAuthenticationService: SSOAuthenticationServiceProtocol // MARK: Private @@ -53,7 +53,7 @@ final class SSOAuthenticationPresenter: NSObject { // MARK: - Setup - init(ssoAuthenticationService: SSOAuthenticationService) { + init(ssoAuthenticationService: SSOAuthenticationServiceProtocol) { self.ssoAuthenticationService = ssoAuthenticationService super.init() } diff --git a/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift b/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift index 706c3782d1..cb3d36d075 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift @@ -22,8 +22,16 @@ enum SSOAuthenticationServiceError: Error { case unknown } +@objc protocol SSOAuthenticationServiceProtocol { + var callBackURLScheme: String? { get } + + func authenticationURL(for identityProvider: String?, transactionId: String) -> URL? + + func loginToken(from url: URL) -> String? +} + @objcMembers -final class SSOAuthenticationService: NSObject { +final class SSOAuthenticationService: NSObject, SSOAuthenticationServiceProtocol { // MARK: - Constants diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m index e1fd5c5e82..665f970acc 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -45,7 +45,7 @@ }; -@interface ManageSessionViewController () +@interface ManageSessionViewController () { // The device to display MXDevice *device; @@ -64,6 +64,8 @@ @interface ManageSessionViewController () +ChangePasswordCoordinatorBridgePresenterDelegate, +SSOAuthenticationPresenterDelegate> { // Current alert (if any). __weak UIAlertController *currentAlert; @@ -300,6 +301,8 @@ @interface SettingsViewController () UserOtherSessionsCoordinator { + let shouldShowDeviceLogout = parameters.session.homeserverWellknown.authentication == nil let parameters = UserOtherSessionsCoordinatorParameters(sessionInfos: sessionInfos, filter: filter, - title: title) + title: title, + showDeviceLogout: shouldShowDeviceLogout) return UserOtherSessionsCoordinator(parameters: parameters) } private func openDeviceLogoutRedirectURL(_ url: URL) { let alert = UIAlertController(title: VectorL10n.manageSessionRedirect, message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default) { [weak self] _ in - UIApplication.shared.open(url) { [weak self] success in - guard success else { - return - } - self?.popToSessionsOverview() - } + guard let self else { return } + + let service = SSOAccountService(accountURL: url) + let presenter = SSOAuthenticationPresenter(ssoAuthenticationService: service) + presenter.delegate = self + self.ssoAuthenticationPresenter = presenter + + presenter.present(forIdentityProvider: nil, with: "", from: self.toPresentable(), animated: true) }) alert.popoverPresentationController?.sourceView = toPresentable().view navigationRouter.present(alert, animated: true) @@ -547,3 +552,25 @@ private extension UserOtherSessionsFilter { } } } + +// MARK: ASWebAuthenticationPresentationContextProviding + +extension UserSessionsFlowCoordinator: SSOAuthenticationPresenterDelegate { + func ssoAuthenticationPresenterDidCancel(_ presenter: SSOAuthenticationPresenter) { + ssoAuthenticationPresenter = nil + MXLog.info("OIDC account management complete.") + popToSessionsOverview() + } + + func ssoAuthenticationPresenter(_ presenter: SSOAuthenticationPresenter, authenticationDidFailWithError error: Error) { + ssoAuthenticationPresenter = nil + MXLog.error("OIDC account management failed.") + } + + func ssoAuthenticationPresenter(_ presenter: SSOAuthenticationPresenter, + authenticationSucceededWithToken token: String, + usingIdentityProvider identityProvider: SSOIdentityProvider?) { + ssoAuthenticationPresenter = nil + MXLog.warning("Unexpected callback after OIDC account management.") + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift index 8f9dab072a..a848a2bf89 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift @@ -21,6 +21,7 @@ struct UserOtherSessionsCoordinatorParameters { let sessionInfos: [UserSessionInfo] let filter: UserOtherSessionsFilter let title: String + let showDeviceLogout: Bool } final class UserOtherSessionsCoordinator: Coordinator, Presentable { @@ -40,6 +41,7 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable { let viewModel = UserOtherSessionsViewModel(sessionInfos: parameters.sessionInfos, filter: parameters.filter, title: parameters.title, + showDeviceLogout: parameters.showDeviceLogout, settingsService: RiotSettings.shared) let view = UserOtherSessions(viewModel: viewModel.context) userOtherSessionsViewModel = viewModel diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift index e81bb7f050..e357e77d03 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift @@ -49,26 +49,31 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { viewModel = UserOtherSessionsViewModel(sessionInfos: allSessions(), filter: .all, title: VectorL10n.userSessionsOverviewOtherSessionsSectionTitle, + showDeviceLogout: true, settingsService: MockUserSessionSettings()) case .none: viewModel = UserOtherSessionsViewModel(sessionInfos: [], filter: .all, title: VectorL10n.userSessionsOverviewOtherSessionsSectionTitle, + showDeviceLogout: true, settingsService: MockUserSessionSettings()) case .inactiveSessions: viewModel = UserOtherSessionsViewModel(sessionInfos: inactiveSessions(), filter: .inactive, title: VectorL10n.userOtherSessionSecurityRecommendationTitle, + showDeviceLogout: true, settingsService: MockUserSessionSettings()) case .unverifiedSessions: viewModel = UserOtherSessionsViewModel(sessionInfos: unverifiedSessions(), filter: .unverified, title: VectorL10n.userOtherSessionSecurityRecommendationTitle, + showDeviceLogout: true, settingsService: MockUserSessionSettings()) case .verifiedSessions: viewModel = UserOtherSessionsViewModel(sessionInfos: verifiedSessions(), filter: .verified, title: VectorL10n.userOtherSessionSecurityRecommendationTitle, + showDeviceLogout: true, settingsService: MockUserSessionSettings()) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift index 270891d912..58d8b2dc7b 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -63,7 +63,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: VectorL10n.userOtherSessionNoInactiveSessions, allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -81,7 +82,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: "", allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -104,7 +106,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: VectorL10n.userOtherSessionNoUnverifiedSessions, allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(expectedItems.count, 2) XCTAssertEqual(sut.state, expectedState) } @@ -123,7 +126,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: VectorL10n.userOtherSessionNoVerifiedSessions, allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -139,7 +143,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: VectorL10n.userOtherSessionNoVerifiedSessions, allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -155,7 +160,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: VectorL10n.userOtherSessionNoUnverifiedSessions, allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -171,7 +177,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: VectorL10n.userOtherSessionNoInactiveSessions, allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -192,7 +199,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: "", allItemsSelected: true, enableSignOutButton: true, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -213,7 +221,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: "", allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -233,7 +242,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: "", allItemsSelected: false, enableSignOutButton: true, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -253,7 +263,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: "", allItemsSelected: true, enableSignOutButton: true, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -273,7 +284,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: "", allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -296,7 +308,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { emptyItemsTitle: "", allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: false) + showLocationInfo: false, + showDeviceLogout: true) XCTAssertEqual(sut.state, expectedState) } @@ -352,6 +365,7 @@ class UserOtherSessionsViewModelTests: XCTestCase { UserOtherSessionsViewModel(sessionInfos: sessionInfos, filter: filter, title: title, + showDeviceLogout: true, settingsService: MockUserSessionSettings()) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index deeb5ab955..d486e1cc4c 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -43,6 +43,7 @@ struct UserOtherSessionsViewState: BindableState, Equatable { var allItemsSelected: Bool var enableSignOutButton: Bool var showLocationInfo: Bool + var showDeviceLogout: Bool } struct UserOtherSessionsBindings: Equatable { diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 84ea6f9ad1..abfb5e99fe 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -28,6 +28,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi init(sessionInfos: [UserSessionInfo], filter: UserOtherSessionsFilter, title: String, + showDeviceLogout: Bool, settingsService: UserSessionSettingsProtocol) { self.sessionInfos = sessionInfos defaultTitle = title @@ -41,7 +42,8 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi emptyItemsTitle: filter.userOtherSessionsViewEmptyResultsTitle, allItemsSelected: false, enableSignOutButton: false, - showLocationInfo: settingsService.showIPAddressesInSessionsManager)) + showLocationInfo: settingsService.showIPAddressesInSessionsManager, + showDeviceLogout: showDeviceLogout)) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 41d79fe547..92eb505b03 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -60,6 +60,7 @@ struct UserOtherSessions: View { set: { _ in withAnimation { viewModel.send(viewAction: .showLocationInfo) } }), allItemsSelected: viewModel.viewState.allItemsSelected, sessionCount: viewModel.viewState.sessionItems.count, + showDeviceLogout: viewModel.viewState.showDeviceLogout, onToggleSelection: { viewModel.send(viewAction: .toggleAllSelection) }, onSignOut: { viewModel.send(viewAction: .logoutAllUserSessions) }) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift index 331ede0c3b..ba844904ec 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift @@ -24,6 +24,7 @@ struct UserOtherSessionsToolbar: ToolbarContent { @Binding var isShowLocationEnabled: Bool let allItemsSelected: Bool let sessionCount: Int + let showDeviceLogout: Bool let onToggleSelection: () -> Void let onSignOut: () -> Void @@ -81,12 +82,14 @@ struct UserOtherSessionsToolbar: ToolbarContent { private func optionsMenu() -> some View { Button { } label: { Menu { - Button { - isEditModeEnabled = true - } label: { - Label(VectorL10n.userOtherSessionMenuSelectSessions, systemImage: "checkmark.circle") + if showDeviceLogout { // As you can only sign out the selected sessions, we don't allow selection when you're unable to sign out devices. + Button { + isEditModeEnabled = true + } label: { + Label(VectorL10n.userOtherSessionMenuSelectSessions, systemImage: "checkmark.circle") + } + .disabled(sessionCount == 0) } - .disabled(sessionCount == 0) Button { isShowLocationEnabled.toggle() @@ -94,7 +97,7 @@ struct UserOtherSessionsToolbar: ToolbarContent { Label(showLocationInfo: isShowLocationEnabled) } - if sessionCount > 0 { + if sessionCount > 0, showDeviceLogout { DestructiveButton { onSignOut() } label: { diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index 7d3d0437c2..20a88aaf12 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -39,7 +39,10 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { self.parameters = parameters service = parameters.service - viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: parameters.service, settingsService: RiotSettings.shared) + let shouldShowDeviceLogout = parameters.session.homeserverWellknown.authentication == nil + viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: parameters.service, + settingsService: RiotSettings.shared, + showDeviceLogout: shouldShowDeviceLogout) hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context)) hostingViewController.vc_setLargeTitleDisplayMode(.never) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift index e09586b833..6132febae2 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift @@ -51,7 +51,7 @@ enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable { fatalError() } - let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service, settingsService: MockUserSessionSettings()) + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service, settingsService: MockUserSessionSettings(), showDeviceLogout: true) return ( [service, viewModel], diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift index 8ce396a6ec..27237983fc 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift @@ -21,7 +21,9 @@ import XCTest class UserSessionsOverviewViewModelTests: XCTestCase { func testInitialStateEmpty() { - let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService(), settingsService: MockUserSessionSettings()) + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService(), + settingsService: MockUserSessionSettings(), + showDeviceLogout: true) XCTAssertNil(viewModel.state.currentSessionViewData) XCTAssertTrue(viewModel.state.unverifiedSessionsViewData.isEmpty) @@ -31,7 +33,9 @@ class UserSessionsOverviewViewModelTests: XCTestCase { } func testLoadOnDidAppear() { - let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService(), settingsService: MockUserSessionSettings()) + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService(), + settingsService: MockUserSessionSettings(), + showDeviceLogout: true) viewModel.process(viewAction: .viewAppeared) XCTAssertNotNil(viewModel.state.currentSessionViewData) @@ -42,7 +46,9 @@ class UserSessionsOverviewViewModelTests: XCTestCase { } func testSimpleActionProcessing() { - let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService(), settingsService: MockUserSessionSettings()) + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService(), + settingsService: MockUserSessionSettings(), + showDeviceLogout: true) var result: UserSessionsOverviewViewModelResult? viewModel.completion = { action in @@ -69,7 +75,9 @@ class UserSessionsOverviewViewModelTests: XCTestCase { let service = MockUserSessionsOverviewService() service.updateOverviewData { _ in } - let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service, settingsService: MockUserSessionSettings()) + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service, + settingsService: MockUserSessionSettings(), + showDeviceLogout: true) var result: UserSessionsOverviewViewModelResult? viewModel.completion = { action in diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift index a7429f12f0..74f35a087f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -51,6 +51,7 @@ struct UserSessionsOverviewViewState: BindableState { var showLoadingIndicator = false var linkDeviceButtonVisible = false var showLocationInfo: Bool + var showDeviceLogout: Bool } enum UserSessionsOverviewViewAction { diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index a2d92628a6..2441260080 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -24,11 +24,11 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess var completion: ((UserSessionsOverviewViewModelResult) -> Void)? - init(userSessionsOverviewService: UserSessionsOverviewServiceProtocol, settingsService: UserSessionSettingsProtocol) { + init(userSessionsOverviewService: UserSessionsOverviewServiceProtocol, settingsService: UserSessionSettingsProtocol, showDeviceLogout: Bool) { self.userSessionsOverviewService = userSessionsOverviewService self.settingsService = settingsService - super.init(initialViewState: .init(showLocationInfo: settingsService.showIPAddressesInSessionsManager)) + super.init(initialViewState: .init(showLocationInfo: settingsService.showIPAddressesInSessionsManager, showDeviceLogout: showDeviceLogout)) userSessionsOverviewService.overviewDataPublisher.sink { [weak self] overviewData in self?.updateViewState(with: overviewData) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 9cfc89ca48..c78718346f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -125,7 +125,7 @@ struct UserSessionsOverview: View { Label(VectorL10n.signOut, systemImage: "rectangle.portrait.and.arrow.right.fill") } } - if viewModel.viewState.otherSessionsViewData.count > 0 { + if viewModel.viewState.otherSessionsViewData.count > 0, viewModel.viewState.showDeviceLogout { DestructiveButton { viewModel.send(viewAction: .logoutOtherSessions) } label: { @@ -149,10 +149,12 @@ struct UserSessionsOverview: View { Label(showLocationInfo: viewModel.viewState.showLocationInfo) } - DestructiveButton { - viewModel.send(viewAction: .logoutOtherSessions) - } label: { - Label(VectorL10n.userOtherSessionMenuSignOutSessions(String(viewModel.viewState.otherSessionsViewData.count)), systemImage: "rectangle.portrait.and.arrow.forward.fill") + if viewModel.viewState.showDeviceLogout { + DestructiveButton { + viewModel.send(viewAction: .logoutOtherSessions) + } label: { + Label(VectorL10n.userOtherSessionMenuSignOutSessions(String(viewModel.viewState.otherSessionsViewData.count)), systemImage: "rectangle.portrait.and.arrow.forward.fill") + } } } label: { menuImage