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

[PM-18594] [RC] Hide coach marks if user has existing login items #1390

Open
wants to merge 2 commits into
base: release/2025.02-rc7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,11 @@ extension VaultListProcessor {
if !value.isEmpty, await services.configService.getFeatureFlag(.nativeCreateAccountFlow) {
await setImportLoginsProgress(.complete)
}
// Dismiss the coach mark action cards once the vault has at least one login item in it.
if await services.configService.getFeatureFlag(.nativeCreateAccountFlow), value.hasLoginItems {
await services.stateService.setLearnNewLoginActionCardStatus(.complete)
await services.stateService.setLearnGeneratorActionCardStatus(.complete)
}
}
} catch {
services.errorReporter.log(error: error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,54 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
task.cancel()
}

/// `perform(_:)` with `.streamVaultList` dismisses the coach marks if the vault contains any
/// login items.
@MainActor
func test_perform_streamVaultList_coachMarkDismiss_vaultContainsLogins() async throws {
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
stateService.activeAccount = .fixture()

let task = Task {
await subject.perform(.streamVaultList)
}
defer { task.cancel() }

let section = VaultListSection(
id: "1",
items: [.fixtureGroup(id: "1", group: .login, count: 1)],
name: "Section"
)
vaultRepository.vaultListSubject.send([section])

try await waitForAsync { self.subject.state.loadingState == .data([section]) }
XCTAssertEqual(stateService.learnGeneratorActionCardStatus, .complete)
XCTAssertEqual(stateService.learnNewLoginActionCardStatus, .complete)
}

/// `perform(_:)` with `.streamVaultList` doesn't dismiss the coach marks if the vault contains
/// no login items.
@MainActor
func test_perform_streamVaultList_coachMarkDismiss_vaultWithoutLogins() async throws {
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
stateService.activeAccount = .fixture()

let task = Task {
await subject.perform(.streamVaultList)
}
defer { task.cancel() }

let section = VaultListSection(
id: "1",
items: [.fixtureGroup(id: "1", group: .card, count: 1)],
name: "Section"
)
vaultRepository.vaultListSubject.send([section])

try await waitForAsync { self.subject.state.loadingState == .data([section]) }
XCTAssertNil(stateService.learnGeneratorActionCardStatus)
XCTAssertNil(stateService.learnNewLoginActionCardStatus)
}

/// `perform(_:)` with `.streamVaultList` updates the state's vault list whenever it changes.
@MainActor
func test_perform_streamVaultList_doesntNeedSync() throws {
Expand Down
18 changes: 18 additions & 0 deletions BitwardenShared/UI/Vault/Vault/VaultList/VaultListSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,21 @@ public struct VaultListSection: Equatable, Identifiable, Sendable {
/// The name of the section, displayed as section header.
public let name: String
}

// MARK: - [VaultListSection]

extension [VaultListSection] {
/// Returns whether any login items exist within the vault list sections.
var hasLoginItems: Bool {
flatMap(\.items)
.contains { item in
if case let .group(group, count) = item.itemType, group == .login || group == .totp {
count > 0 // swiftlint:disable:this empty_count
} else if case let .cipher(cipherView, _) = item.itemType, cipherView.type == .login {
true
} else {
false
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import XCTest

@testable import BitwardenShared

class VaultListSectionTests: BitwardenTestCase {
// MARK: Tests

/// `hasLoginItems` returns `false` if there's no login items within the sections.
func test_vaultListSectionArray_hasLoginItems_false() {
let subjectEmpty = [VaultListSection]()
XCTAssertFalse(subjectEmpty.hasLoginItems)

let subjectWithoutLogin = [
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
VaultListSection(id: "3", items: [.fixtureGroup(group: .identity, count: 3)], name: "Identities"),
VaultListSection(id: "4", items: [.fixtureGroup(group: .secureNote, count: 0)], name: "Notes"),
]
XCTAssertFalse(subjectWithoutLogin.hasLoginItems)

let subjectLoginsEmpty = [
VaultListSection(id: "1", items: [.fixtureGroup(group: .login, count: 0)], name: "Logins"),
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
VaultListSection(id: "3", items: [.fixtureGroup(group: .identity, count: 3)], name: "Identities"),
VaultListSection(id: "4", items: [.fixtureGroup(group: .secureNote, count: 0)], name: "Notes"),
]
XCTAssertFalse(subjectLoginsEmpty.hasLoginItems)

let subjectCiphersNoLogins = [
VaultListSection(id: "5", items: [.fixture(cipherView: .fixture(type: .identity))], name: "Items"),
]
XCTAssertFalse(subjectCiphersNoLogins.hasLoginItems)
}

/// `hasLoginItems` returns `true` if there's a login group with more than one item or a login
/// cipher within the sections.
func test_vaultListSectionArray_hasLoginItems_true() {
let subjectWithLogin = [
VaultListSection(id: "1", items: [.fixtureGroup(group: .login, count: 1)], name: "Logins"),
]
XCTAssertTrue(subjectWithLogin.hasLoginItems)

let subjectWithMultipleSections = [
VaultListSection(id: "1", items: [.fixtureGroup(group: .login, count: 1)], name: "Logins"),
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
VaultListSection(id: "3", items: [.fixtureGroup(group: .identity, count: 3)], name: "Identities"),
VaultListSection(id: "4", items: [.fixtureGroup(group: .secureNote, count: 0)], name: "Notes"),
]
XCTAssertTrue(subjectWithMultipleSections.hasLoginItems)

let subjectWithCipher = [
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
VaultListSection(id: "5", items: [.fixture(cipherView: .fixture(type: .login))], name: "Items"),
]
XCTAssertTrue(subjectWithCipher.hasLoginItems)

let subjectWithTOTP = [
VaultListSection(id: "2", items: [.fixtureGroup(group: .totp, count: 2)], name: "TOTP"),
]
XCTAssertTrue(subjectWithTOTP.hasLoginItems)
}
}