Skip to content

Commit bd9c04e

Browse files
[PM-18594] [RC] Hide coach marks if user has existing login items (#1390)
1 parent d3c2592 commit bd9c04e

File tree

4 files changed

+139
-0
lines changed

4 files changed

+139
-0
lines changed

BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessor.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,11 @@ extension VaultListProcessor {
434434
if !value.isEmpty, await services.configService.getFeatureFlag(.nativeCreateAccountFlow) {
435435
await setImportLoginsProgress(.complete)
436436
}
437+
// Dismiss the coach mark action cards once the vault has at least one login item in it.
438+
if await services.configService.getFeatureFlag(.nativeCreateAccountFlow), value.hasLoginItems {
439+
await services.stateService.setLearnNewLoginActionCardStatus(.complete)
440+
await services.stateService.setLearnGeneratorActionCardStatus(.complete)
441+
}
437442
}
438443
} catch {
439444
services.errorReporter.log(error: error)

BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,54 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
830830
task.cancel()
831831
}
832832

833+
/// `perform(_:)` with `.streamVaultList` dismisses the coach marks if the vault contains any
834+
/// login items.
835+
@MainActor
836+
func test_perform_streamVaultList_coachMarkDismiss_vaultContainsLogins() async throws {
837+
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
838+
stateService.activeAccount = .fixture()
839+
840+
let task = Task {
841+
await subject.perform(.streamVaultList)
842+
}
843+
defer { task.cancel() }
844+
845+
let section = VaultListSection(
846+
id: "1",
847+
items: [.fixtureGroup(id: "1", group: .login, count: 1)],
848+
name: "Section"
849+
)
850+
vaultRepository.vaultListSubject.send([section])
851+
852+
try await waitForAsync { self.subject.state.loadingState == .data([section]) }
853+
XCTAssertEqual(stateService.learnGeneratorActionCardStatus, .complete)
854+
XCTAssertEqual(stateService.learnNewLoginActionCardStatus, .complete)
855+
}
856+
857+
/// `perform(_:)` with `.streamVaultList` doesn't dismiss the coach marks if the vault contains
858+
/// no login items.
859+
@MainActor
860+
func test_perform_streamVaultList_coachMarkDismiss_vaultWithoutLogins() async throws {
861+
configService.featureFlagsBool[.nativeCreateAccountFlow] = true
862+
stateService.activeAccount = .fixture()
863+
864+
let task = Task {
865+
await subject.perform(.streamVaultList)
866+
}
867+
defer { task.cancel() }
868+
869+
let section = VaultListSection(
870+
id: "1",
871+
items: [.fixtureGroup(id: "1", group: .card, count: 1)],
872+
name: "Section"
873+
)
874+
vaultRepository.vaultListSubject.send([section])
875+
876+
try await waitForAsync { self.subject.state.loadingState == .data([section]) }
877+
XCTAssertNil(stateService.learnGeneratorActionCardStatus)
878+
XCTAssertNil(stateService.learnNewLoginActionCardStatus)
879+
}
880+
833881
/// `perform(_:)` with `.streamVaultList` updates the state's vault list whenever it changes.
834882
@MainActor
835883
func test_perform_streamVaultList_doesntNeedSync() throws {

BitwardenShared/UI/Vault/Vault/VaultList/VaultListSection.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,23 @@ public struct VaultListSection: Equatable, Identifiable, Sendable {
1212
/// The name of the section, displayed as section header.
1313
public let name: String
1414
}
15+
16+
// MARK: - [VaultListSection]
17+
18+
extension [VaultListSection] {
19+
/// Returns whether any login items exist within the vault list sections.
20+
var hasLoginItems: Bool {
21+
flatMap(\.items)
22+
.contains { item in
23+
if case let .group(group, count) = item.itemType, group == .login || group == .totp {
24+
count > 0 // swiftlint:disable:this empty_count
25+
} else if case let .cipher(cipherView, _) = item.itemType, cipherView.type == .login {
26+
true
27+
} else if case .totp = item.itemType {
28+
true
29+
} else {
30+
false
31+
}
32+
}
33+
}
34+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import XCTest
2+
3+
@testable import BitwardenShared
4+
5+
class VaultListSectionTests: BitwardenTestCase {
6+
// MARK: Tests
7+
8+
/// `hasLoginItems` returns `false` if there's no login items within the sections.
9+
func test_vaultListSectionArray_hasLoginItems_false() {
10+
let subjectEmpty = [VaultListSection]()
11+
XCTAssertFalse(subjectEmpty.hasLoginItems)
12+
13+
let subjectWithoutLogin = [
14+
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
15+
VaultListSection(id: "3", items: [.fixtureGroup(group: .identity, count: 3)], name: "Identities"),
16+
VaultListSection(id: "4", items: [.fixtureGroup(group: .secureNote, count: 0)], name: "Notes"),
17+
]
18+
XCTAssertFalse(subjectWithoutLogin.hasLoginItems)
19+
20+
let subjectLoginsEmpty = [
21+
VaultListSection(id: "1", items: [.fixtureGroup(group: .login, count: 0)], name: "Logins"),
22+
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
23+
VaultListSection(id: "3", items: [.fixtureGroup(group: .identity, count: 3)], name: "Identities"),
24+
VaultListSection(id: "4", items: [.fixtureGroup(group: .secureNote, count: 0)], name: "Notes"),
25+
]
26+
XCTAssertFalse(subjectLoginsEmpty.hasLoginItems)
27+
28+
let subjectCiphersNoLogins = [
29+
VaultListSection(id: "5", items: [.fixture(cipherView: .fixture(type: .identity))], name: "Items"),
30+
]
31+
XCTAssertFalse(subjectCiphersNoLogins.hasLoginItems)
32+
}
33+
34+
/// `hasLoginItems` returns `true` if there's a login group with more than one item or a login
35+
/// cipher within the sections.
36+
func test_vaultListSectionArray_hasLoginItems_true() {
37+
let subjectWithLogin = [
38+
VaultListSection(id: "1", items: [.fixtureGroup(group: .login, count: 1)], name: "Logins"),
39+
]
40+
XCTAssertTrue(subjectWithLogin.hasLoginItems)
41+
42+
let subjectWithMultipleSections = [
43+
VaultListSection(id: "1", items: [.fixtureGroup(group: .login, count: 1)], name: "Logins"),
44+
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
45+
VaultListSection(id: "3", items: [.fixtureGroup(group: .identity, count: 3)], name: "Identities"),
46+
VaultListSection(id: "4", items: [.fixtureGroup(group: .secureNote, count: 0)], name: "Notes"),
47+
]
48+
XCTAssertTrue(subjectWithMultipleSections.hasLoginItems)
49+
50+
let subjectWithCipher = [
51+
VaultListSection(id: "2", items: [.fixtureGroup(group: .card, count: 2)], name: "Cards"),
52+
VaultListSection(id: "5", items: [.fixture(cipherView: .fixture(type: .login))], name: "Items"),
53+
]
54+
XCTAssertTrue(subjectWithCipher.hasLoginItems)
55+
56+
let subjectWithTOTP = [
57+
VaultListSection(id: "1", items: [.fixtureTOTP(totp: .fixture())], name: "TOTP"),
58+
]
59+
XCTAssertTrue(subjectWithTOTP.hasLoginItems)
60+
61+
let subjectWithTOTPGroup = [
62+
VaultListSection(id: "2", items: [.fixtureGroup(group: .totp, count: 2)], name: "TOTP"),
63+
]
64+
XCTAssertTrue(subjectWithTOTPGroup.hasLoginItems)
65+
}
66+
}

0 commit comments

Comments
 (0)