QR login from device manager (#6818)

* Add link device button into the sessions overview screen

* Run Swift format

* Fix tests

* Fix a crash in tests

* Fix PR remark
This commit is contained in:
ismailgulek
2022-10-07 12:58:26 +03:00
committed by GitHub
parent f15e9928b8
commit 09124c2243
33 changed files with 223 additions and 126 deletions
@@ -69,6 +69,8 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
self.showCurrentSessionOverview(sessionInfo: sessionInfo)
case let .showUserSessionOverview(sessionInfo):
self.showUserSessionOverview(sessionInfo: sessionInfo)
case .linkDevice:
self.completion?(.linkDevice)
}
}
}
@@ -107,5 +109,4 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
private func showUserSessionOverview(sessionInfo: UserSessionInfo) {
completion?(.openSessionOverview(sessionInfo: sessionInfo))
}
}
@@ -47,4 +47,10 @@ class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
func accountData(for eventType: String) -> [AnyHashable: Any]? {
session.accountData.accountData(forEventType: eventType)
}
func qrLoginAvailable() async throws -> Bool {
let service = QRLoginService(client: session.matrixRestClient,
mode: .authenticated)
return try await service.isServiceAvailable()
}
}
@@ -29,4 +29,6 @@ protocol UserSessionsDataProviderProtocol {
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo?
func accountData(for eventType: String) -> [AnyHashable: Any]?
func qrLoginAvailable() async throws -> Bool
}
@@ -32,7 +32,8 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
overviewData = UserSessionsOverviewData(currentSession: nil,
unverifiedSessions: [],
inactiveSessions: [],
otherSessions: [])
otherSessions: [],
linkDeviceEnabled: false)
sessionInfos = []
setupInitialOverviewData()
}
@@ -44,8 +45,12 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
switch response {
case .success(let devices):
self.sessionInfos = self.sortedSessionInfos(from: devices)
self.overviewData = self.sessionsOverviewData(from: self.sessionInfos)
completion(.success(self.overviewData))
Task { @MainActor in
let linkDeviceEnabled = try? await self.dataProvider.qrLoginAvailable()
self.overviewData = self.sessionsOverviewData(from: self.sessionInfos,
linkDeviceEnabled: linkDeviceEnabled ?? false)
completion(.success(self.overviewData))
}
case .failure(let error):
completion(.failure(error))
}
@@ -59,7 +64,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
return overviewData.otherSessions.first(where: { $0.id == sessionId })
}
// MARK: - Private
private func setupInitialOverviewData() {
@@ -70,7 +75,8 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo],
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
otherSessions: [])
otherSessions: [],
linkDeviceEnabled: false)
}
private func getCurrentSessionInfo() -> UserSessionInfo? {
@@ -87,11 +93,13 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) }
}
private func sessionsOverviewData(from allSessions: [UserSessionInfo]) -> UserSessionsOverviewData {
private func sessionsOverviewData(from allSessions: [UserSessionInfo],
linkDeviceEnabled: Bool) -> UserSessionsOverviewData {
UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
unverifiedSessions: allSessions.filter { !$0.isVerified },
inactiveSessions: allSessions.filter { !$0.isActive },
otherSessions: allSessions.filter { !$0.isCurrent })
otherSessions: allSessions.filter { !$0.isCurrent },
linkDeviceEnabled: linkDeviceEnabled)
}
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
@@ -17,7 +17,6 @@
import Foundation
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
enum Mode {
case currentSessionUnverified
case currentSessionVerified
@@ -37,7 +36,8 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
overviewData = UserSessionsOverviewData(currentSession: nil,
unverifiedSessions: [],
inactiveSessions: [],
otherSessions: [])
otherSessions: [],
linkDeviceEnabled: false)
}
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
@@ -49,24 +49,28 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
overviewData = UserSessionsOverviewData(currentSession: currentSession,
unverifiedSessions: [],
inactiveSessions: [],
otherSessions: [])
otherSessions: [],
linkDeviceEnabled: false)
case .onlyUnverifiedSessions:
overviewData = UserSessionsOverviewData(currentSession: currentSession,
unverifiedSessions: unverifiedSessions + [currentSession],
inactiveSessions: [],
otherSessions: unverifiedSessions)
otherSessions: unverifiedSessions,
linkDeviceEnabled: false)
case .onlyInactiveSessions:
overviewData = UserSessionsOverviewData(currentSession: currentSession,
unverifiedSessions: [],
inactiveSessions: inactiveSessions,
otherSessions: inactiveSessions)
otherSessions: inactiveSessions,
linkDeviceEnabled: false)
default:
let otherSessions = unverifiedSessions + inactiveSessions + buildSessions(verified: true, active: true)
overviewData = UserSessionsOverviewData(currentSession: currentSession,
unverifiedSessions: unverifiedSessions,
inactiveSessions: inactiveSessions,
otherSessions: otherSessions)
otherSessions: otherSessions,
linkDeviceEnabled: true)
}
completion(.success(overviewData))
@@ -75,7 +79,7 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
overviewData.otherSessions.first { $0.id == sessionId }
}
// MARK: - Private
private var currentSession: UserSessionInfo {
@@ -103,7 +107,7 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
deviceType: .desktop,
isVerified: verified,
lastSeenIP: "1.0.0.1",
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
lastSeenTimestamp: Date().timeIntervalSince1970 - 8_000_000,
applicationName: "Element MacOS",
applicationVersion: "1.0.0",
applicationURL: nil,
@@ -21,6 +21,7 @@ struct UserSessionsOverviewData {
let unverifiedSessions: [UserSessionInfo]
let inactiveSessions: [UserSessionInfo]
let otherSessions: [UserSessionInfo]
let linkDeviceEnabled: Bool
}
protocol UserSessionsOverviewServiceProtocol {
@@ -23,6 +23,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
XCTAssertTrue(app.buttons["userSessionCardVerifyButton"].exists)
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
verifyLinkDeviceButtonStatus(true)
}
func testCurrentSessionVerified() {
@@ -30,6 +32,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
XCTAssertFalse(app.buttons["userSessionCardVerifyButton"].exists)
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
verifyLinkDeviceButtonStatus(true)
}
func testOnlyUnverifiedSessions() {
@@ -37,6 +41,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
verifyLinkDeviceButtonStatus(false)
}
func testOnlyInactiveSessions() {
@@ -44,6 +50,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
verifyLinkDeviceButtonStatus(false)
}
func testNoOtherSessions() {
@@ -51,5 +59,18 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
XCTAssertFalse(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
XCTAssertFalse(app.staticTexts["userSessionsOverviewOtherSection"].exists)
verifyLinkDeviceButtonStatus(false)
}
func verifyLinkDeviceButtonStatus(_ enabled: Bool) {
if enabled {
let linkDeviceButton = app.buttons["linkDeviceButton"]
XCTAssertTrue(linkDeviceButton.exists)
XCTAssertTrue(linkDeviceButton.isEnabled)
} else {
let linkDeviceButton = app.buttons["linkDeviceButton"]
XCTAssertFalse(linkDeviceButton.exists)
}
}
}
@@ -27,6 +27,7 @@ class UserSessionsOverviewViewModelTests: XCTestCase {
XCTAssertTrue(viewModel.state.unverifiedSessionsViewData.isEmpty)
XCTAssertTrue(viewModel.state.inactiveSessionsViewData.isEmpty)
XCTAssertTrue(viewModel.state.otherSessionsViewData.isEmpty)
XCTAssertFalse(viewModel.state.linkDeviceButtonVisible)
}
func testLoadOnDidAppear() {
@@ -37,6 +38,7 @@ class UserSessionsOverviewViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.state.unverifiedSessionsViewData.isEmpty)
XCTAssertFalse(viewModel.state.inactiveSessionsViewData.isEmpty)
XCTAssertFalse(viewModel.state.otherSessionsViewData.isEmpty)
XCTAssertTrue(viewModel.state.linkDeviceButtonVisible)
}
func testSimpleActionProcessing() {
@@ -52,6 +54,9 @@ class UserSessionsOverviewViewModelTests: XCTestCase {
viewModel.process(viewAction: .viewAllInactiveSessions)
XCTAssertEqual(result, .showOtherSessions(sessionInfos: [], filter: .inactive))
viewModel.process(viewAction: .linkDevice)
XCTAssertEqual(result, .linkDevice)
}
func testShowSessionDetails() {
@@ -23,6 +23,7 @@ enum UserSessionsOverviewCoordinatorResult {
case logoutOfSession(UserSessionInfo)
case openSessionOverview(sessionInfo: UserSessionInfo)
case openOtherSessions(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter)
case linkDevice
}
// MARK: View model
@@ -34,6 +35,7 @@ enum UserSessionsOverviewViewModelResult: Equatable {
case logoutOfSession(UserSessionInfo)
case showCurrentSessionOverview(sessionInfo: UserSessionInfo)
case showUserSessionOverview(sessionInfo: UserSessionInfo)
case linkDevice
}
// MARK: View
@@ -48,6 +50,8 @@ struct UserSessionsOverviewViewState: BindableState {
var otherSessionsViewData = [UserSessionListItemViewData]()
var showLoadingIndicator = false
var linkDeviceButtonVisible = false
}
enum UserSessionsOverviewViewAction {
@@ -60,4 +64,5 @@ enum UserSessionsOverviewViewAction {
case viewAllInactiveSessions
case viewAllOtherSessions
case tapUserSession(_ sessionId: String)
case linkDevice
}
@@ -70,6 +70,8 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
return
}
completion?(.showUserSessionOverview(sessionInfo: session))
case .linkDevice:
completion?(.linkDevice)
}
}
@@ -83,6 +85,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
if let currentSessionInfo = userSessionsViewData.currentSession {
state.currentSessionViewData = UserSessionCardViewData(sessionInfo: currentSessionInfo)
}
state.linkDeviceButtonVisible = userSessionsViewData.linkDeviceEnabled
}
private func loadData() {
@@ -113,6 +116,6 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
extension Collection where Element == UserSessionInfo {
func asViewData() -> [UserSessionListItemViewData] {
map { UserSessionListItemViewDataFactory().create(from: $0)}
map { UserSessionListItemViewDataFactory().create(from: $0) }
}
}
@@ -18,7 +18,6 @@ import Foundation
/// View data for UserSessionListItem
struct UserSessionListItemViewData: Identifiable, Hashable {
var id: String {
sessionId
}
@@ -17,7 +17,6 @@
import Foundation
struct UserSessionListItemViewDataFactory {
func create(from sessionInfo: UserSessionInfo, highlightSessionDetails: Bool = false) -> UserSessionListItemViewData {
let sessionName = UserSessionNameFormatter.sessionName(deviceType: sessionInfo.deviceType,
sessionDisplayName: sessionInfo.name)
@@ -41,7 +40,7 @@ struct UserSessionListItemViewDataFactory {
}
private func inactiveSessionDetails(sessionInfo: UserSessionInfo) -> String {
if let lastActivityDate = sessionInfo.lastSeenTimestamp {
if let lastActivityDate = sessionInfo.lastSeenTimestamp {
let lastActivityDateString = InactiveUserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
return VectorL10n.userInactiveSessionItemWithDate(lastActivityDateString)
}
@@ -22,15 +22,25 @@ struct UserSessionsOverview: View {
@ObservedObject var viewModel: UserSessionsOverviewViewModel.Context
var body: some View {
ScrollView {
if hasSecurityRecommendations {
securityRecommendationsSection
}
currentSessionsSection
if !viewModel.viewState.otherSessionsViewData.isEmpty {
otherSessionsSection
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 0) {
ScrollView {
if hasSecurityRecommendations {
securityRecommendationsSection
}
currentSessionsSection
if !viewModel.viewState.otherSessionsViewData.isEmpty {
otherSessionsSection
}
}
.readableFrame()
if viewModel.viewState.linkDeviceButtonVisible {
linkDeviceView
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36)
}
}
}
.background(theme.colors.system.ignoresSafeArea())
@@ -158,6 +168,23 @@ struct UserSessionsOverview: View {
}
.accessibilityIdentifier("userSessionsOverviewOtherSection")
}
/// The footer view containing link device button.
var linkDeviceView: some View {
VStack {
Button {
viewModel.send(viewAction: .linkDevice)
} label: {
Text(VectorL10n.userSessionsOverviewLinkDevice)
}
.buttonStyle(PrimaryActionButtonStyle(font: theme.fonts.bodySB))
.padding(.top, 28)
.padding(.bottom, 12)
.padding(.horizontal, 16)
.accessibilityIdentifier("linkDeviceButton")
}
.background(theme.colors.system.ignoresSafeArea())
}
}
// MARK: - Previews