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-16141: Learn Generator action card #1273

Merged
merged 12 commits into from
Jan 31, 2025
20 changes: 20 additions & 0 deletions BitwardenShared/Core/Platform/Services/StateService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ protocol StateService: AnyObject {
///
func getLastUserShouldConnectToWatch() async -> Bool

/// Gets the status of Learn Generator Action Card.
///
/// - Returns: The status of Learn generator Action Card.
///
func getLearnGeneratorActionCardStatus() async -> AccountSetupProgress?

/// Get any pending login request data.
///
/// - Returns: The pending login request data from a push notification.
Expand Down Expand Up @@ -513,6 +519,12 @@ protocol StateService: AnyObject {
///
func setIntroCarouselShown(_ shown: Bool) async

/// Sets the status of Learn generator Action Card.
///
/// - Parameter status: The status of Learn generator Action Card.
///
func setLearnGeneratorActionCardStatus(_ status: AccountSetupProgress) async

/// Sets the last active time within the app.
///
/// - Parameters:
Expand Down Expand Up @@ -1543,6 +1555,10 @@ actor DefaultStateService: StateService { // swiftlint:disable:this type_body_le
appSettingsStore.accountCreationEnvironmentURLs(email: email)
}

func getLearnGeneratorActionCardStatus() async -> AccountSetupProgress? {
appSettingsStore.learnGeneratorActionCardStatus
}

func getPreAuthServerConfig() async -> ServerConfig? {
appSettingsStore.preAuthServerConfig
}
Expand Down Expand Up @@ -1780,6 +1796,10 @@ actor DefaultStateService: StateService { // swiftlint:disable:this type_body_le
lastSyncTimeByUserIdSubject.value[userId] = date
}

func setLearnGeneratorActionCardStatus(_ status: AccountSetupProgress) async {
appSettingsStore.learnGeneratorActionCardStatus = status
ezimet-livefront marked this conversation as resolved.
Show resolved Hide resolved
}

func setLoginRequest(_ loginRequest: LoginRequestNotification?) async {
appSettingsStore.loginRequest = loginRequest
}
Expand Down
19 changes: 19 additions & 0 deletions BitwardenShared/Core/Platform/Services/StateServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,16 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body
XCTAssertEqual(lastSyncTime, date)
}

/// `getLearnGeneratorActionCardStatus()` returns the status of the learn generator action card.
func test_getLearnNewLoginActionCardStatus() async {
var learnGeneratorActionCardStatus = await subject.getLearnGeneratorActionCardStatus()
XCTAssertEqual(learnGeneratorActionCardStatus, .incomplete)

appSettingsStore.learnGeneratorActionCardStatus = .complete
learnGeneratorActionCardStatus = await subject.getLearnGeneratorActionCardStatus()
XCTAssertEqual(learnGeneratorActionCardStatus, .complete)
}

/// `getLoginRequest()` gets any pending login requests.
func test_getLoginRequest() async {
let loginRequest = LoginRequestNotification(id: "1", userId: "10")
Expand Down Expand Up @@ -1762,6 +1772,15 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body
)
}

/// `setLearnGeneratorActionCardStatus(_:)` sets the learn generator action card status.
func test_setLearnNewLoginActionCardStatus() async {
await subject.setLearnGeneratorActionCardStatus(.incomplete)
XCTAssertEqual(appSettingsStore.learnGeneratorActionCardStatus, .incomplete)

await subject.setLearnGeneratorActionCardStatus(.complete)
XCTAssertEqual(appSettingsStore.learnGeneratorActionCardStatus, .complete)
}

/// `setLoginRequest()` sets the pending login requests.
func test_setLoginRequest() async {
let loginRequest = LoginRequestNotification(id: "1", userId: "10")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ protocol AppSettingsStore: AnyObject {
/// sending the status to the watch if the user is logged out.
var lastUserShouldConnectToWatch: Bool { get set }

/// The status of the learn generator action card.
var learnGeneratorActionCardStatus: AccountSetupProgress { get set }

/// The login request information received from a push notification.
var loginRequest: LoginRequestNotification? { get set }

Expand Down Expand Up @@ -712,6 +715,7 @@ extension DefaultAppSettingsStore: AppSettingsStore {
case lastActiveTime(userId: String)
case lastSync(userId: String)
case lastUserShouldConnectToWatch
case learnGeneratorActionCardStatus
case loginRequest
case manuallyLockedAccount(userId: String)
case masterPasswordHash(userId: String)
Expand Down Expand Up @@ -787,6 +791,8 @@ extension DefaultAppSettingsStore: AppSettingsStore {
key = "lastActiveTime_\(userId)"
case let .lastSync(userId):
key = "lastSync_\(userId)"
case .learnGeneratorActionCardStatus:
key = "learnGeneratorActionCardStatus"
case .lastUserShouldConnectToWatch:
key = "lastUserShouldConnectToWatch"
case .loginRequest:
Expand Down Expand Up @@ -881,6 +887,11 @@ extension DefaultAppSettingsStore: AppSettingsStore {
set { store(newValue, for: .lastUserShouldConnectToWatch) }
}

var learnGeneratorActionCardStatus: AccountSetupProgress {
get { fetch(for: .learnGeneratorActionCardStatus) ?? .incomplete }
set { store(newValue, for: .learnGeneratorActionCardStatus) }
}

var loginRequest: LoginRequestNotification? {
get { fetch(for: .loginRequest) }
set { store(newValue, for: .loginRequest) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,29 @@ class AppSettingsStoreTests: BitwardenTestCase { // swiftlint:disable:this type_
XCTAssertEqual(userDefaults.double(forKey: "bwPreferencesStorage:lastSync_2"), 1_685_664_000.0)
}

/// `learnGeneratorActionCardStatus` returns `.incomplete` if there isn't a previously stored value.
func test_learnGeneratorActionCardStatus_isInitiallyIncomplete() {
XCTAssertEqual(subject.learnGeneratorActionCardStatus, .incomplete)
}

/// `learnGeneratorActionCardStatus` can be used to get and set the persisted value in user defaults.
func test_learnGeneratorActionCardStatus_withValues() {
subject.learnGeneratorActionCardStatus = .complete
XCTAssertEqual(subject.learnGeneratorActionCardStatus, .complete)

try XCTAssertEqual(
JSONDecoder().decode(
AccountSetupProgress.self,
from: XCTUnwrap(
userDefaults
.string(forKey: "bwPreferencesStorage:learnGeneratorActionCardStatus")?
.data(using: .utf8)
)
),
AccountSetupProgress.complete
)
}

/// `loginRequest` returns `nil` if there isn't a previously stored value.
func test_loginRequest_isInitiallyNil() {
XCTAssertNil(subject.loginRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class MockAppSettingsStore: AppSettingsStore { // swiftlint:disable:this type_bo
var disableWebIcons = false
var introCarouselShown = false
var lastUserShouldConnectToWatch = false
var learnGeneratorActionCardStatus: AccountSetupProgress = .incomplete
var loginRequest: LoginRequestNotification?
var migrationVersion = 0
var overrideDebugFeatureFlagCalled = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
var isAuthenticated = [String: Bool]()
var isAuthenticatedError: Error?
var lastActiveTime = [String: Date]()
var learnGeneratorActionCardStatus: AccountSetupProgress?
var loginRequest: LoginRequestNotification?
var logoutAccountUserInitiated = false
var getAccountEncryptionKeysError: Error?
Expand Down Expand Up @@ -260,6 +261,10 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
return lastSyncTimeByUserId[userId]
}

func getLearnGeneratorActionCardStatus() async -> AccountSetupProgress? {
learnGeneratorActionCardStatus
}

func getLoginRequest() async -> LoginRequestNotification? {
loginRequest
}
Expand Down Expand Up @@ -519,6 +524,10 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt
lastUserShouldConnectToWatch
}

func setLearnGeneratorActionCardStatus(_ status: AccountSetupProgress) async {
learnGeneratorActionCardStatus = status
}

func setLoginRequest(_ loginRequest: LoginRequestNotification?) async {
self.loginRequest = loginRequest
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,8 @@
"UnknownAccount" = "Unknown account";
"UserVerificationForPasskey" = "User verification for passkey";
"NewItem" = "New item";
"ExploreTheGenerator" = "Explore the generator";
"LearnMoreAboutGeneratingSecureLoginCredentialsWithAGuidedTour" = "Learn more about generating secure login credentials with a guided tour.";
"ExportVaultFilePwProtectInfo" = "This file export will be password protected and require the file password to decrypt.";
"IntroCarouselPage1Title" = "Security, prioritized";
"IntroCarouselPage1Message" = "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect whatโ€™s important to you.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class BitwardenSliderTests: BitwardenTestCase {
func test_snapshot_slider_minValue() {
let subject = BitwardenSlider(
value: .constant(0),
in: 0...50,
in: 0 ... 50,
step: 1,
onEditingChanged: { _ in }
)
Expand All @@ -26,7 +26,7 @@ class BitwardenSliderTests: BitwardenTestCase {
func test_snapshot_slider_midValue() {
let subject = BitwardenSlider(
value: .constant(25),
in: 0...50,
in: 0 ... 50,
step: 1,
onEditingChanged: { _ in }
)
Expand All @@ -40,7 +40,7 @@ class BitwardenSliderTests: BitwardenTestCase {
func test_snapshot_slider_maxValue() {
let subject = BitwardenSlider(
value: .constant(50),
in: 0...50,
in: 0 ... 50,
step: 1,
onEditingChanged: { _ in }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@
enum GeneratorEffect {
/// The generator view appeared on screen.
case appeared

/// The user tapped the dismiss button on the learn generator action card.
case dismissLearnGeneratorActionCard

/// Show the learn generator guided tour.
case showLearnGeneratorGuidedTour
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import OSLog
final class GeneratorProcessor: StateProcessor<GeneratorState, GeneratorAction, GeneratorEffect> {
// MARK: Types

typealias Services = HasErrorReporter
typealias Services = HasConfigService
& HasErrorReporter
& HasGeneratorRepository
& HasPasteboardService
& HasPolicyService
& HasReviewPromptService
& HasStateService

/// The behavior that should be taken after receiving a new action for generating a new value
/// and persisting it.
Expand Down Expand Up @@ -78,6 +80,13 @@ final class GeneratorProcessor: StateProcessor<GeneratorState, GeneratorAction,
switch effect {
case .appeared:
await generateValue(shouldSavePassword: true)
await checkLearnGeneratorActionCardEligibility()
case .dismissLearnGeneratorActionCard:
await services.stateService.setLearnGeneratorActionCardStatus(.complete)
state.isLearnGeneratorActionCardEligible = false
case .showLearnGeneratorGuidedTour:
await services.stateService.setLearnGeneratorActionCardStatus(.complete)
state.isLearnGeneratorActionCardEligible = false
}
}

Expand Down Expand Up @@ -175,6 +184,15 @@ final class GeneratorProcessor: StateProcessor<GeneratorState, GeneratorAction,

// MARK: Private

/// Checks the eligibility of the generator Login action card.
///
private func checkLearnGeneratorActionCardEligibility() async {
if await services.configService.getFeatureFlag(.nativeCreateAccountFlow) {
state.isLearnGeneratorActionCardEligible = await services.stateService
.getLearnGeneratorActionCardStatus() == .incomplete
}
}

/// Generate a new passphrase.
///
/// - Parameter settings: The passphrase generator settings used to generate a new password.
Expand Down
Loading
Loading