From 67ee5d99192f0fcf72f7e619a54df62aea8a8de1 Mon Sep 17 00:00:00 2001 From: Matt Czech Date: Thu, 19 Sep 2024 12:06:52 -0500 Subject: [PATCH] PM-10524: Persist user's autofill and vault unlock setup progress in create account flow (#944) --- .../Core/Auth/Services/AuthService.swift | 20 ++- .../Core/Auth/Services/AuthServiceTests.swift | 92 ++++++++++++- .../TestHelpers/MockAuthService.swift | 9 +- .../Models/Enum/AccountSetupProgress.swift | 14 ++ .../Core/Platform/Services/StateService.swift | 123 ++++++++++++------ .../Platform/Services/StateServiceTests.swift | 90 ++++++++----- .../Services/Stores/AppSettingsStore.swift | 78 +++++++---- .../Stores/AppSettingsStoreTests.swift | 49 ++++--- .../TestHelpers/MockAppSettingsStore.swift | 27 ++-- .../TestHelpers/MockStateService.swift | 33 +++-- BitwardenShared/UI/Auth/AuthCoordinator.swift | 13 +- .../UI/Auth/AuthCoordinatorTests.swift | 21 ++- BitwardenShared/UI/Auth/AuthRoute.swift | 6 +- BitwardenShared/UI/Auth/AuthRouterTests.swift | 21 +++ .../CompleteRegistrationProcessor.swift | 6 +- .../CompleteRegistrationProcessorTests.swift | 5 +- .../Extensions/AuthRouter+Redirects.swift | 7 + .../UI/Auth/Login/LoginProcessor.swift | 3 +- .../UI/Auth/Login/LoginProcessorTests.swift | 28 +++- .../UI/Auth/Login/LoginState.swift | 3 + .../VaultUnlockSetupProcessor.swift | 11 +- .../VaultUnlockSetupProcessorTests.swift | 27 +++- 22 files changed, 520 insertions(+), 166 deletions(-) create mode 100644 BitwardenShared/Core/Platform/Models/Enum/AccountSetupProgress.swift diff --git a/BitwardenShared/Core/Auth/Services/AuthService.swift b/BitwardenShared/Core/Auth/Services/AuthService.swift index 6ddf4a533..9442cd910 100644 --- a/BitwardenShared/Core/Auth/Services/AuthService.swift +++ b/BitwardenShared/Core/Auth/Services/AuthService.swift @@ -158,8 +158,14 @@ protocol AuthService { /// - password: The master password. /// - username: The username. /// - captchaToken: An optional captcha token value to add to the token request. + /// - isNewAccount: Whether the user is logging into a newly created account. /// - func loginWithMasterPassword(_ password: String, username: String, captchaToken: String?) async throws + func loginWithMasterPassword( + _ password: String, + username: String, + captchaToken: String?, + isNewAccount: Bool + ) async throws /// Login with the single sign on code. /// @@ -538,7 +544,12 @@ class DefaultAuthService: AuthService { // swiftlint:disable:this type_body_leng return (loginWithDeviceData.privateKey, key) } - func loginWithMasterPassword(_ masterPassword: String, username: String, captchaToken: String?) async throws { + func loginWithMasterPassword( + _ masterPassword: String, + username: String, + captchaToken: String?, + isNewAccount: Bool + ) async throws { // Complete the pre-login steps. let response = try await accountAPIService.preLogin(email: username) @@ -573,6 +584,11 @@ class DefaultAuthService: AuthService { // swiftlint:disable:this type_body_leng if try await requirePasswordChange(email: username, masterPassword: masterPassword, policy: policy) { try await stateService.setForcePasswordResetReason(.weakMasterPasswordOnLogin) } + + if isNewAccount, await configService.getFeatureFlag(.nativeCreateAccountFlow) { + try await stateService.setAccountSetupAutofill(.incomplete) + try await stateService.setAccountSetupVaultUnlock(.incomplete) + } } /// Check TDE user decryption options to see if can unlock with trusted deviceKey or needs diff --git a/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift b/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift index d0c623945..8f9c397ec 100644 --- a/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift +++ b/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift @@ -331,7 +331,77 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ try await subject.loginWithMasterPassword( "Password1234!", username: "email@example.com", - captchaToken: nil + captchaToken: nil, + isNewAccount: false + ) + + // Verify the results. + let preLoginRequest = PreLoginRequestModel( + email: "email@example.com" + ) + let tokenRequest = IdentityTokenRequestModel( + authenticationMethod: .password(username: "email@example.com", password: "hashed password"), + captchaToken: nil, + deviceInfo: DeviceInfo( + identifier: "App id", + name: "Model id" + ), + loginRequestId: nil + ) + XCTAssertEqual(client.requests.count, 2) + XCTAssertEqual(client.requests[0].body, try preLoginRequest.encode()) + XCTAssertEqual(client.requests[1].body, try tokenRequest.encode()) + + XCTAssertEqual(clientService.mockAuth.hashPasswordEmail, "user@bitwarden.com") + XCTAssertEqual(clientService.mockAuth.hashPasswordPassword, "Password1234!") + XCTAssertEqual(clientService.mockAuth.hashPasswordKdfParams, .pbkdf2(iterations: 600_000)) + + XCTAssertEqual(stateService.accountsAdded, [Account.fixtureAccountLogin()]) + XCTAssertEqual( + stateService.accountEncryptionKeys, + [ + "13512467-9cfe-43b0-969f-07534084764b": AccountEncryptionKeys( + encryptedPrivateKey: "PRIVATE_KEY", + encryptedUserKey: "KEY" + ), + ] + ) + XCTAssertNil(stateService.accountSetupAutofill["13512467-9cfe-43b0-969f-07534084764b"]) + XCTAssertNil(stateService.accountSetupVaultUnlock["13512467-9cfe-43b0-969f-07534084764b"]) + XCTAssertEqual( + stateService.masterPasswordHashes, + ["13512467-9cfe-43b0-969f-07534084764b": "hashed password"] + ) + try XCTAssertEqual( + keychainRepository.getValue(for: .accessToken(userId: "13512467-9cfe-43b0-969f-07534084764b")), + IdentityTokenResponseModel.fixture().accessToken + ) + try XCTAssertEqual( + keychainRepository.getValue(for: .refreshToken(userId: "13512467-9cfe-43b0-969f-07534084764b")), + IdentityTokenResponseModel.fixture().refreshToken + ) + assertGetConfig() + } + + /// `loginWithMasterPassword(_:username:captchaToken:)` logs the user in with the password for + /// a newly created account. + func test_loginWithMasterPassword_isNewAccount() async throws { // swiftlint:disable:this function_body_length + client.results = [ + .httpSuccess(testData: .preLoginSuccess), + .httpSuccess(testData: .identityTokenSuccess), + ] + appSettingsStore.appId = "App id" + clientService.mockAuth.hashPasswordResult = .success("hashed password") + configService.featureFlagsBool[.nativeCreateAccountFlow] = true + stateService.preAuthEnvironmentUrls = EnvironmentUrlData(base: URL(string: "https://vault.bitwarden.com")) + systemDevice.modelIdentifier = "Model id" + + // Attempt to login. + try await subject.loginWithMasterPassword( + "Password1234!", + username: "email@example.com", + captchaToken: nil, + isNewAccount: true ) // Verify the results. @@ -365,6 +435,8 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ ), ] ) + XCTAssertEqual(stateService.accountSetupAutofill["13512467-9cfe-43b0-969f-07534084764b"], .incomplete) + XCTAssertEqual(stateService.accountSetupVaultUnlock["13512467-9cfe-43b0-969f-07534084764b"], .incomplete) XCTAssertEqual( stateService.masterPasswordHashes, ["13512467-9cfe-43b0-969f-07534084764b": "hashed password"] @@ -398,7 +470,8 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ try await subject.loginWithMasterPassword( "Password1234!", username: "email@example.com", - captchaToken: nil + captchaToken: nil, + isNewAccount: false ) // Verify the results. @@ -461,7 +534,8 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ try await subject.loginWithMasterPassword( "Password1234!", username: "email@example.com", - captchaToken: nil + captchaToken: nil, + isNewAccount: false ) } @@ -607,7 +681,8 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ try await subject.loginWithMasterPassword( "Password1234!", username: "email@example.com", - captchaToken: nil + captchaToken: nil, + isNewAccount: false ) } @@ -675,7 +750,8 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ try await subject.loginWithMasterPassword( "Password1234!", username: "email@example.com", - captchaToken: nil + captchaToken: nil, + isNewAccount: false ) } @@ -713,7 +789,8 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ try await subject.loginWithMasterPassword( "Password1234!", username: "email@example.com", - captchaToken: nil + captchaToken: nil, + isNewAccount: false ) } @@ -823,7 +900,8 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ try await subject.loginWithMasterPassword( "Password1234!", username: "email@example.com", - captchaToken: nil + captchaToken: nil, + isNewAccount: false ) } diff --git a/BitwardenShared/Core/Auth/Services/TestHelpers/MockAuthService.swift b/BitwardenShared/Core/Auth/Services/TestHelpers/MockAuthService.swift index 3226765ef..803138d4e 100644 --- a/BitwardenShared/Core/Auth/Services/TestHelpers/MockAuthService.swift +++ b/BitwardenShared/Core/Auth/Services/TestHelpers/MockAuthService.swift @@ -45,6 +45,7 @@ class MockAuthService: AuthService { var loginWithMasterPasswordPassword: String? var loginWithMasterPasswordUsername: String? var loginWithMasterPasswordCaptchaToken: String? + var loginWithMasterPasswordIsNewAccount = false var loginWithMasterPasswordResult: Result = .success(()) var loginWithSingleSignOnCode: String? @@ -125,10 +126,16 @@ class MockAuthService: AuthService { return try loginWithDeviceResult.get() } - func loginWithMasterPassword(_ password: String, username: String, captchaToken: String?) async throws { + func loginWithMasterPassword( + _ password: String, + username: String, + captchaToken: String?, + isNewAccount: Bool + ) async throws { loginWithMasterPasswordPassword = password loginWithMasterPasswordUsername = username loginWithMasterPasswordCaptchaToken = captchaToken + loginWithMasterPasswordIsNewAccount = isNewAccount try loginWithMasterPasswordResult.get() } diff --git a/BitwardenShared/Core/Platform/Models/Enum/AccountSetupProgress.swift b/BitwardenShared/Core/Platform/Models/Enum/AccountSetupProgress.swift new file mode 100644 index 000000000..30bb3775b --- /dev/null +++ b/BitwardenShared/Core/Platform/Models/Enum/AccountSetupProgress.swift @@ -0,0 +1,14 @@ +// MARK: - AccountSetupProgress + +/// An enum to represent a user's progress towards setting up new account functionality. +/// +enum AccountSetupProgress: Int, Codable { + /// The user hasn't yet made any progress. + case incomplete = 0 + + /// The user choose to set up the functionality later. + case setUpLater = 1 + + /// The user has completed the set up. + case complete = 2 +} diff --git a/BitwardenShared/Core/Platform/Services/StateService.swift b/BitwardenShared/Core/Platform/Services/StateService.swift index e496600dc..6774b9dab 100644 --- a/BitwardenShared/Core/Platform/Services/StateService.swift +++ b/BitwardenShared/Core/Platform/Services/StateService.swift @@ -53,6 +53,20 @@ protocol StateService: AnyObject { /// - Parameter userId: The user ID of the account. Defaults to the active account if `nil`. func getAccountHasBeenUnlockedInteractively(userId: String?) async throws -> Bool + /// Gets the user's progress for setting up autofill. + /// + /// - Parameter userId: The user ID associated with the autofill setup progress. + /// - Returns: The user's autofill setup progress. + /// + func getAccountSetupAutofill(userId: String?) async throws -> AccountSetupProgress? + + /// Gets the user's progress for setting up vault unlock. + /// + /// - Parameter userId: The user ID associated with the vault unlock setup progress. + /// - Returns: The user's vault unlock setup progress. + /// + func getAccountSetupVaultUnlock(userId: String?) async throws -> AccountSetupProgress? + /// Gets all accounts. /// /// - Returns: The known user accounts. @@ -204,13 +218,6 @@ protocol StateService: AnyObject { /// func getMasterPasswordHash(userId: String?) async throws -> String? - /// Gets whether the user needs to set up vault unlock methods. - /// - /// - Parameter userId: The user ID associated with the value. - /// - Returns: Whether the user needs to set up vault unlock methods. - /// - func getNeedsVaultUnlockSetup(userId: String?) async throws -> Bool - /// Gets the last notifications registration date for a user ID. /// /// - Parameter userId: The user ID of the account. Defaults to the active account if `nil`. @@ -354,6 +361,22 @@ protocol StateService: AnyObject { /// - value: Whether the user has unlocked their account in the current session. func setAccountHasBeenUnlockedInteractively(userId: String?, value: Bool) async throws + /// Sets the user's progress for setting up autofill. + /// + /// - Parameters: + /// - autofillSetup: The user's autofill setup progress. + /// - userId: The user ID associated with the autofill setup progress. + /// + func setAccountSetupAutofill(_ autofillSetup: AccountSetupProgress?, userId: String?) async throws + + /// Sets the user's progress for setting up vault unlock. + /// + /// - Parameters: + /// - autofillSetup: The user's vault unlock setup progress. + /// - userId: The user ID associated with the vault unlock setup progress. + /// + func setAccountSetupVaultUnlock(_ vaultUnlockSetup: AccountSetupProgress?, userId: String?) async throws + /// Sets the active account. /// /// - Parameter userId: The user Id of the account to set as active. @@ -478,14 +501,6 @@ protocol StateService: AnyObject { /// func setMasterPasswordHash(_ hash: String?, userId: String?) async throws - /// Sets whether the user needs to set up vault unlock methods. - /// - /// - Parameters: - /// - needsVaultUnlockSetup: Whether the user needs to set up vault unlock methods. - /// - userId: The user ID associated with the value. - /// - func setNeedsVaultUnlockSetup(_ needsVaultUnlockSetup: Bool, userId: String?) async throws - /// Sets the last notifications registration date for a user ID. /// /// - Parameters: @@ -677,6 +692,22 @@ extension StateService { try await getAccount(userId: userId).profile.userId } + /// Gets the active user's progress for setting up autofill. + /// + /// - Returns: The user's autofill setup progress. + /// + func getAccountSetupAutofill() async throws -> AccountSetupProgress? { + try await getAccountSetupAutofill(userId: nil) + } + + /// Gets the active user's progress for setting up vault unlock. + /// + /// - Returns: The user's vault unlock setup progress. + /// + func getAccountSetupVaultUnlock() async throws -> AccountSetupProgress? { + try await getAccountSetupVaultUnlock(userId: nil) + } + /// Gets the active account id. /// /// - Returns: The active user id. @@ -779,14 +810,6 @@ extension StateService { try await getMasterPasswordHash(userId: nil) } - /// Gets whether the active account needs to set up vault unlock methods. - /// - /// - Returns: Whether the user needs to set up vault unlock methods. - /// - func getNeedsVaultUnlockSetup() async throws -> Bool { - try await getNeedsVaultUnlockSetup(userId: nil) - } - /// Gets the last notifications registration date for the active account. /// /// - Returns: The last notifications registration date for the active account. @@ -901,6 +924,22 @@ extension StateService { try await setAccountHasBeenUnlockedInteractively(userId: nil, value: value) } + /// Sets the active user's progress for setting up autofill. + /// + /// - Parameter autofillSetup: The user's autofill setup progress. + /// + func setAccountSetupAutofill(_ autofillSetup: AccountSetupProgress?) async throws { + try await setAccountSetupAutofill(autofillSetup, userId: nil) + } + + /// Sets the active user's progress for setting up vault unlock. + /// + /// - Parameter vaultUnlockSetup: The user's vault unlock setup progress. + /// + func setAccountSetupVaultUnlock(_ vaultUnlockSetup: AccountSetupProgress?) async throws { + try await setAccountSetupVaultUnlock(vaultUnlockSetup, userId: nil) + } + /// Sets the allow sync on refresh value for the active account. /// /// - Parameter allowSyncOnRefresh: The allow sync on refresh value. @@ -973,14 +1012,6 @@ extension StateService { try await setMasterPasswordHash(hash, userId: nil) } - /// Sets whether the active account needs to set up vault unlock methods. - /// - /// - Parameter needsVaultUnlockSetup: Whether the user needs to set up vault unlock methods. - /// - func setNeedsVaultUnlockSetup(_ needsVaultUnlockSetup: Bool) async throws { - try await setNeedsVaultUnlockSetup(needsVaultUnlockSetup, userId: nil) - } - /// Sets the last notifications registration date for the active account. /// /// - Parameter date: The last notifications registration date. @@ -1196,6 +1227,16 @@ actor DefaultStateService: StateService { // swiftlint:disable:this type_body_le return accountVolatileData[userId]?.hasBeenUnlockedInteractively == true } + func getAccountSetupAutofill(userId: String?) async throws -> AccountSetupProgress? { + let userId = try userId ?? getActiveAccountUserId() + return appSettingsStore.accountSetupAutofill(userId: userId) + } + + func getAccountSetupVaultUnlock(userId: String?) async throws -> AccountSetupProgress? { + let userId = try userId ?? getActiveAccountUserId() + return appSettingsStore.accountSetupVaultUnlock(userId: userId) + } + func getAccounts() throws -> [Account] { guard let accounts = appSettingsStore.state?.accounts else { throw StateServiceError.noAccounts @@ -1285,11 +1326,6 @@ actor DefaultStateService: StateService { // swiftlint:disable:this type_body_le return appSettingsStore.masterPasswordHash(userId: userId) } - func getNeedsVaultUnlockSetup(userId: String?) async throws -> Bool { - let userId = try userId ?? getActiveAccountUserId() - return appSettingsStore.needsVaultUnlockSetup(userId: userId) - } - func getNotificationsLastRegistrationDate(userId: String?) async throws -> Date? { let userId = try userId ?? getActiveAccountUserId() return appSettingsStore.notificationsLastRegistrationDate(userId: userId) @@ -1428,6 +1464,16 @@ actor DefaultStateService: StateService { // swiftlint:disable:this type_body_le ].hasBeenUnlockedInteractively = value } + func setAccountSetupAutofill(_ autofillSetup: AccountSetupProgress?, userId: String?) async throws { + let userId = try userId ?? getActiveAccountUserId() + appSettingsStore.setAccountSetupAutofill(autofillSetup, userId: userId) + } + + func setAccountSetupVaultUnlock(_ vaultUnlockSetup: AccountSetupProgress?, userId: String?) async throws { + let userId = try userId ?? getActiveAccountUserId() + appSettingsStore.setAccountSetupVaultUnlock(vaultUnlockSetup, userId: userId) + } + func setActiveAccount(userId: String) async throws { guard var state = appSettingsStore.state else { return } defer { appSettingsStore.state = state } @@ -1515,11 +1561,6 @@ actor DefaultStateService: StateService { // swiftlint:disable:this type_body_le appSettingsStore.setMasterPasswordHash(hash, userId: userId) } - func setNeedsVaultUnlockSetup(_ needsVaultUnlockSetup: Bool, userId: String?) async throws { - let userId = try userId ?? getActiveAccountUserId() - appSettingsStore.setNeedsVaultUnlockSetup(needsVaultUnlockSetup, userId: userId) - } - func setNotificationsLastRegistrationDate(_ date: Date?, userId: String?) async throws { let userId = try userId ?? getActiveAccountUserId() appSettingsStore.setNotificationsLastRegistrationDate(date, userId: userId) diff --git a/BitwardenShared/Core/Platform/Services/StateServiceTests.swift b/BitwardenShared/Core/Platform/Services/StateServiceTests.swift index 9968b3093..647611cd3 100644 --- a/BitwardenShared/Core/Platform/Services/StateServiceTests.swift +++ b/BitwardenShared/Core/Platform/Services/StateServiceTests.swift @@ -300,6 +300,44 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body } } + /// `getAccountSetupAutofill()` returns the user's autofill setup progress. + func test_getAccountSetupAutofill() async throws { + await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) + + let initialValue = try await subject.getAccountSetupAutofill() + XCTAssertNil(initialValue) + + appSettingsStore.accountSetupAutofill["1"] = .setUpLater + let setUpLater = try await subject.getAccountSetupAutofill() + XCTAssertEqual(setUpLater, .setUpLater) + } + + /// `getAccountSetupAutofill()` throws an error if there isn't an active account. + func test_getAccountSetupAutofill_noAccount() async throws { + await assertAsyncThrows(error: StateServiceError.noActiveAccount) { + _ = try await subject.getAccountSetupAutofill() + } + } + + /// `getAccountSetupVaultUnlock()` returns the user's vault unlock setup progress. + func test_getAccountSetupVaultUnlock() async throws { + await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) + + let initialValue = try await subject.getAccountSetupVaultUnlock() + XCTAssertNil(initialValue) + + appSettingsStore.accountSetupVaultUnlock["1"] = .setUpLater + let setUpLater = try await subject.getAccountSetupVaultUnlock() + XCTAssertEqual(setUpLater, .setUpLater) + } + + /// `getAccountSetupVaultUnlock()` throws an error if there isn't an active account. + func test_getAccountSetupVaultUnlock_noAccount() async throws { + await assertAsyncThrows(error: StateServiceError.noActiveAccount) { + _ = try await subject.getAccountSetupVaultUnlock() + } + } + /// `getActiveAccount()` returns the active account. func test_getActiveAccount() async throws { let account = Account.fixture(profile: .fixture(userId: "2")) @@ -637,25 +675,6 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body } } - /// `getNeedsVaultUnlockSetup()` returns whether the user needs to set up vault unlock methods. - func test_getNeedsVaultUnlockSetup() async throws { - await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) - - let initialValue = try await subject.getNeedsVaultUnlockSetup() - XCTAssertFalse(initialValue) - - appSettingsStore.needsVaultUnlockSetup["1"] = true - let needsVaultUnlockSetup = try await subject.getNeedsVaultUnlockSetup() - XCTAssertTrue(needsVaultUnlockSetup) - } - - /// `getNeedsVaultUnlockSetup()` throws an error if there isn't an active account. - func test_getNeedsVaultUnlockSetup_noAccount() async throws { - await assertAsyncThrows(error: StateServiceError.noActiveAccount) { - _ = try await subject.getNeedsVaultUnlockSetup() - } - } - /// `getNotificationsLastRegistrationDate()` returns the user's last notifications registration date. func test_getNotificationsLastRegistrationDate() async throws { await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) @@ -1507,6 +1526,28 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body } } + /// `setAccountSetupAutofill(_:)` sets the user's autofill setup progress. + func test_setAccountSetupAutofill() async throws { + await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) + + try await subject.setAccountSetupAutofill(.incomplete) + XCTAssertEqual(appSettingsStore.accountSetupAutofill, ["1": .incomplete]) + + try await subject.setAccountSetupAutofill(.complete, userId: "1") + XCTAssertEqual(appSettingsStore.accountSetupAutofill, ["1": .complete]) + } + + /// `setAccountSetupVaultUnlock(_:)` sets the user's vault unlock setup progress. + func test_setAccountSetupVaultUnlock() async throws { + await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) + + try await subject.setAccountSetupVaultUnlock(.incomplete) + XCTAssertEqual(appSettingsStore.accountSetupVaultUnlock, ["1": .incomplete]) + + try await subject.setAccountSetupVaultUnlock(.complete, userId: "1") + XCTAssertEqual(appSettingsStore.accountSetupVaultUnlock, ["1": .complete]) + } + /// `setActiveAccount(userId: )` succeeds if there is a matching account func test_setActiveAccount_match_multi() async throws { let account1 = Account.fixture(profile: .fixture(userId: "1")) @@ -1569,17 +1610,6 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body XCTAssertEqual(appSettingsStore.masterPasswordHashes, ["1": "1234"]) } - /// `setNeedsVaultUnlockSetup(_:)` sets whether the user needs to set up vault unlock methods. - func test_setNeedsVaultUnlockSetup() async throws { - await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) - - try await subject.setNeedsVaultUnlockSetup(true) - XCTAssertEqual(appSettingsStore.needsVaultUnlockSetup, ["1": true]) - - try await subject.setNeedsVaultUnlockSetup(false, userId: "1") - XCTAssertEqual(appSettingsStore.needsVaultUnlockSetup, ["1": false]) - } - /// `setNotificationsLastRegistrationDate(_:)` sets the last notifications registration date for a user. func test_setNotificationsLastRegistrationDate() async throws { await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) diff --git a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift index e90442866..c80533eff 100644 --- a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift +++ b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift @@ -59,6 +59,20 @@ protocol AppSettingsStore: AnyObject { /// The app's account state. var state: State? { get set } + /// The user's progress for setting up autofill. + /// + /// - Parameter userId: The user ID associated with the stored autofill setup progress. + /// - Returns: The user's autofill setup progress. + /// + func accountSetupAutofill(userId: String) -> AccountSetupProgress? + + /// The user's progress for setting up vault unlock. + /// + /// - Parameter userId: The user ID associated with the stored vault unlock setup progress. + /// - Returns: The user's vault unlock setup progress. + /// + func accountSetupVaultUnlock(userId: String) -> AccountSetupProgress? + /// Whether the vault should sync on refreshing. /// /// - Parameter userId: The user ID associated with the sync on refresh setting. @@ -162,13 +176,6 @@ protocol AppSettingsStore: AnyObject { /// func masterPasswordHash(userId: String) -> String? - /// Gets whether the user needs to set up vault unlock methods. - /// - /// - Parameter userId: The user ID associated with the value. - /// - Returns: Whether the user needs to set up vault unlock methods. - /// - func needsVaultUnlockSetup(userId: String) -> Bool - /// Gets the last date the user successfully registered for push notifications. /// /// - Parameter userId: The user ID associated with the last notifications registration date. @@ -204,6 +211,22 @@ protocol AppSettingsStore: AnyObject { /// - Returns: The server config for that user ID. func serverConfig(userId: String) -> ServerConfig? + /// Sets the user's progress for autofill setup. + /// + /// - Parameters: + /// - autofillSetup: The user's autofill setup progress. + /// - userId: The user ID associated with the stored autofill setup progress. + /// + func setAccountSetupAutofill(_ autofillSetup: AccountSetupProgress?, userId: String) + + /// Sets the user's progress for vault unlock setup. + /// + /// - Parameters: + /// - vaultUnlockSetup: The user's vault unlock setup progress. + /// - userId: The user ID associated with the stored autofill setup progress. + /// + func setAccountSetupVaultUnlock(_ vaultUnlockSetup: AccountSetupProgress?, userId: String) + /// Whether the vault should sync on refreshing. /// /// - Parameters: @@ -320,14 +343,6 @@ protocol AppSettingsStore: AnyObject { /// func setMasterPasswordHash(_ hash: String?, userId: String) - /// Sets whether the user needs to set up vault unlock methods. - /// - /// - Parameters: - /// - needsVaultUnlockSetup: Whether the user needs to set up vault unlock methods. - /// - userId: The user ID associated with the value. - /// - func setNeedsVaultUnlockSetup(_ needsVaultUnlockSetup: Bool, userId: String) - /// Sets the last notifications registration date for a user ID. /// /// - Parameters: @@ -594,6 +609,8 @@ extension DefaultAppSettingsStore: AppSettingsStore { /// The keys used to store their associated values. /// enum Keys { + case accountSetupAutofill(userId: String) + case accountSetupVaultUnlock(userId: String) case addSitePromptShown case allowSyncOnRefresh(userId: String) case appId @@ -618,7 +635,6 @@ extension DefaultAppSettingsStore: AppSettingsStore { case loginRequest case masterPasswordHash(userId: String) case migrationVersion - case needsVaultUnlockSetup(userId: String) case notificationsLastRegistrationDate(userId: String) case passwordGenerationOptions(userId: String) case pinProtectedUserKey(userId: String) @@ -641,6 +657,10 @@ extension DefaultAppSettingsStore: AppSettingsStore { var storageKey: String { let key: String switch self { + case let .accountSetupAutofill(userId): + key = "accountSetupAutofill_\(userId)" + case let .accountSetupVaultUnlock(userId): + key = "accountSetupVaultUnlock_\(userId)" case .addSitePromptShown: key = "addSitePromptShown" case let .allowSyncOnRefresh(userId): @@ -689,8 +709,6 @@ extension DefaultAppSettingsStore: AppSettingsStore { key = "keyHash_\(userId)" case .migrationVersion: key = "migrationVersion" - case let .needsVaultUnlockSetup(userId): - key = "needsVaultUnlockSetup_\(userId)" case let .notificationsLastRegistrationDate(userId): key = "pushLastRegistrationDate_\(userId)" case let .passwordGenerationOptions(userId): @@ -808,6 +826,14 @@ extension DefaultAppSettingsStore: AppSettingsStore { } } + func accountSetupAutofill(userId: String) -> AccountSetupProgress? { + fetch(for: .accountSetupAutofill(userId: userId)) + } + + func accountSetupVaultUnlock(userId: String) -> AccountSetupProgress? { + fetch(for: .accountSetupVaultUnlock(userId: userId)) + } + func allowSyncOnRefresh(userId: String) -> Bool { fetch(for: .allowSyncOnRefresh(userId: userId)) } @@ -873,10 +899,6 @@ extension DefaultAppSettingsStore: AppSettingsStore { fetch(for: .masterPasswordHash(userId: userId)) } - func needsVaultUnlockSetup(userId: String) -> Bool { - fetch(for: .needsVaultUnlockSetup(userId: userId)) - } - func notificationsLastRegistrationDate(userId: String) -> Date? { fetch(for: .notificationsLastRegistrationDate(userId: userId)).map { Date(timeIntervalSince1970: $0) } } @@ -899,6 +921,14 @@ extension DefaultAppSettingsStore: AppSettingsStore { fetch(for: .serverConfig(userId: userId)) } + func setAccountSetupAutofill(_ autofillSetup: AccountSetupProgress?, userId: String) { + store(autofillSetup, for: .accountSetupAutofill(userId: userId)) + } + + func setAccountSetupVaultUnlock(_ vaultUnlockSetup: AccountSetupProgress?, userId: String) { + store(vaultUnlockSetup, for: .accountSetupVaultUnlock(userId: userId)) + } + func setAllowSyncOnRefresh(_ allowSyncOnRefresh: Bool?, userId: String) { store(allowSyncOnRefresh, for: .allowSyncOnRefresh(userId: userId)) } @@ -961,10 +991,6 @@ extension DefaultAppSettingsStore: AppSettingsStore { store(hash, for: .masterPasswordHash(userId: userId)) } - func setNeedsVaultUnlockSetup(_ needsVaultUnlockSetup: Bool, userId: String) { - store(needsVaultUnlockSetup, for: .needsVaultUnlockSetup(userId: userId)) - } - func setNotificationsLastRegistrationDate(_ date: Date?, userId: String) { store(date?.timeIntervalSince1970, for: .notificationsLastRegistrationDate(userId: userId)) } diff --git a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift index 7f51c2f12..d7804b175 100644 --- a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift +++ b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift @@ -38,6 +38,38 @@ class AppSettingsStoreTests: BitwardenTestCase { // swiftlint:disable:this type_ // MARK: Tests + /// `accountSetupAutofill(userId:)` returns `nil` if there isn't a previously stored value. + func test_accountSetupAutofill_isInitiallyNil() { + XCTAssertNil(subject.accountSetupAutofill(userId: "-1")) + } + + /// `accountSetupAutofill(userId:)` can be used to get the user's progress for autofill setup. + func test_accountSetupAutofill_withValue() { + subject.setAccountSetupAutofill(.setUpLater, userId: "1") + subject.setAccountSetupAutofill(.complete, userId: "2") + + XCTAssertEqual(subject.accountSetupAutofill(userId: "1"), .setUpLater) + XCTAssertEqual(subject.accountSetupAutofill(userId: "2"), .complete) + XCTAssertEqual(userDefaults.integer(forKey: "bwPreferencesStorage:accountSetupAutofill_1"), 1) + XCTAssertEqual(userDefaults.integer(forKey: "bwPreferencesStorage:accountSetupAutofill_2"), 2) + } + + /// `accountSetupVaultUnlock(userId:)` returns `nil` if there isn't a previously stored value. + func test_accountSetupVaultUnlock_isInitiallyFalse() { + XCTAssertNil(subject.accountSetupVaultUnlock(userId: "-1")) + } + + /// `accountSetupVaultUnlock(userId:)` can be used to get the user's progress for vault unlock setup. + func test_accountSetupVaultUnlock_withValue() { + subject.setAccountSetupVaultUnlock(.setUpLater, userId: "1") + subject.setAccountSetupVaultUnlock(.complete, userId: "2") + + XCTAssertEqual(subject.accountSetupVaultUnlock(userId: "1"), .setUpLater) + XCTAssertEqual(subject.accountSetupVaultUnlock(userId: "2"), .complete) + XCTAssertEqual(userDefaults.integer(forKey: "bwPreferencesStorage:accountSetupVaultUnlock_1"), 1) + XCTAssertEqual(userDefaults.integer(forKey: "bwPreferencesStorage:accountSetupVaultUnlock_2"), 2) + } + /// `addSitePromptShown` returns `false` if there isn't a previously stored value. func test_addSitePromptShown_isInitiallyFalse() { XCTAssertFalse(subject.addSitePromptShown) @@ -515,23 +547,6 @@ class AppSettingsStoreTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertEqual(userDefaults.double(forKey: "bwPreferencesStorage:pushLastRegistrationDate_2"), 1_696_204_800.0) } - /// `needsVaultUnlockSetup(userId:)` returns `false` if there isn't a previously stored value. - func test_needsVaultUnlockSetup_isInitiallyFalse() { - XCTAssertFalse(subject.needsVaultUnlockSetup(userId: "-1")) - } - - /// `needsVaultUnlockSetup(userId:)` can be used to get whether the user needs to set up vault - /// unlock methods. - func test_needsVaultUnlockSetup__withValue() { - subject.setNeedsVaultUnlockSetup(true, userId: "1") - subject.setNeedsVaultUnlockSetup(false, userId: "2") - - XCTAssertTrue(subject.needsVaultUnlockSetup(userId: "1")) - XCTAssertFalse(subject.needsVaultUnlockSetup(userId: "2")) - XCTAssertTrue(userDefaults.bool(forKey: "bwPreferencesStorage:needsVaultUnlockSetup_1")) - XCTAssertFalse(userDefaults.bool(forKey: "bwPreferencesStorage:needsVaultUnlockSetup_2")) - } - /// `passwordGenerationOptions(userId:)` returns `nil` if there isn't a previously stored value. func test_passwordGenerationOptions_isInitiallyNil() { XCTAssertNil(subject.passwordGenerationOptions(userId: "-1")) diff --git a/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift b/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift index 3cd7d607a..857a4db2a 100644 --- a/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift +++ b/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift @@ -4,6 +4,8 @@ import Foundation @testable import BitwardenShared class MockAppSettingsStore: AppSettingsStore { + var accountSetupAutofill = [String: AccountSetupProgress]() + var accountSetupVaultUnlock = [String: AccountSetupProgress]() var addSitePromptShown = false var allowSyncOnRefreshes = [String: Bool]() var appId: String? @@ -33,7 +35,6 @@ class MockAppSettingsStore: AppSettingsStore { var lastActiveTime = [String: Date]() var lastSyncTimeByUserId = [String: Date]() var masterPasswordHashes = [String: String]() - var needsVaultUnlockSetup = [String: Bool]() var notificationsLastRegistrationDates = [String: Date]() var passwordGenerationOptions = [String: PasswordGenerationOptions]() var pinProtectedUserKey = [String: String]() @@ -55,6 +56,14 @@ class MockAppSettingsStore: AppSettingsStore { lazy var activeIdSubject = CurrentValueSubject(self.state?.activeUserId) + func accountSetupAutofill(userId: String) -> AccountSetupProgress? { + accountSetupAutofill[userId] + } + + func accountSetupVaultUnlock(userId: String) -> AccountSetupProgress? { + accountSetupVaultUnlock[userId] + } + func allowSyncOnRefresh(userId: String) -> Bool { allowSyncOnRefreshes[userId] ?? false } @@ -103,10 +112,6 @@ class MockAppSettingsStore: AppSettingsStore { masterPasswordHashes[userId] } - func needsVaultUnlockSetup(userId: String) -> Bool { - needsVaultUnlockSetup[userId] ?? false - } - func notificationsLastRegistrationDate(userId: String) -> Date? { notificationsLastRegistrationDates[userId] } @@ -131,6 +136,14 @@ class MockAppSettingsStore: AppSettingsStore { serverConfig[userId] } + func setAccountSetupAutofill(_ autofillSetup: AccountSetupProgress?, userId: String) { + accountSetupAutofill[userId] = autofillSetup + } + + func setAccountSetupVaultUnlock(_ vaultUnlockSetup: AccountSetupProgress?, userId: String) { + accountSetupVaultUnlock[userId] = vaultUnlockSetup + } + func setAllowSyncOnRefresh(_ allowSyncOnRefresh: Bool?, userId: String) { allowSyncOnRefreshes[userId] = allowSyncOnRefresh } @@ -191,10 +204,6 @@ class MockAppSettingsStore: AppSettingsStore { notificationsLastRegistrationDates[userId] = date } - func setNeedsVaultUnlockSetup(_ needsVaultUnlockSetup: Bool, userId: String) { - self.needsVaultUnlockSetup[userId] = needsVaultUnlockSetup - } - func setPasswordGenerationOptions(_ options: PasswordGenerationOptions?, userId: String) { guard let options else { passwordGenerationOptions.removeValue(forKey: userId) diff --git a/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift b/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift index 1f0b0254e..9e74c6afe 100644 --- a/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift +++ b/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift @@ -5,6 +5,8 @@ import Foundation class MockStateService: StateService { // swiftlint:disable:this type_body_length var accountEncryptionKeys = [String: AccountEncryptionKeys]() + var accountSetupAutofill = [String: AccountSetupProgress]() + var accountSetupVaultUnlock = [String: AccountSetupProgress]() var accountTokens: Account.AccountTokens? var accountVolatileData: [String: AccountVolatileData] = [:] var accountsAdded = [Account]() @@ -49,7 +51,6 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt var lastSyncTimeSubject = CurrentValueSubject(nil) var lastUserShouldConnectToWatch = false var masterPasswordHashes = [String: String]() - var needsVaultUnlockSetup = [String: Bool]() var notificationsLastRegistrationDates = [String: Date]() var notificationsLastRegistrationError: Error? var passwordGenerationOptions = [String: PasswordGenerationOptions]() @@ -145,6 +146,16 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt return accounts } + func getAccountSetupAutofill(userId: String?) async throws -> AccountSetupProgress? { + let userId = try unwrapUserId(userId) + return accountSetupAutofill[userId] + } + + func getAccountSetupVaultUnlock(userId: String?) async throws -> AccountSetupProgress? { + let userId = try unwrapUserId(userId) + return accountSetupVaultUnlock[userId] + } + func getAccountIdOrActiveId(userId: String?) async throws -> String { if let userId { return userId @@ -234,11 +245,6 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt return masterPasswordHashes[userId] } - func getNeedsVaultUnlockSetup(userId: String?) async throws -> Bool { - let userId = try unwrapUserId(userId) - return needsVaultUnlockSetup[userId] ?? false - } - func getNotificationsLastRegistrationDate(userId: String?) async throws -> Date? { if let notificationsLastRegistrationError { throw notificationsLastRegistrationError @@ -342,6 +348,16 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt try setAccountHasBeenUnlockedInteractivelyResult.get() } + func setAccountSetupAutofill(_ autofillSetup: AccountSetupProgress?, userId: String?) async throws { + let userId = try unwrapUserId(userId) + accountSetupAutofill[userId] = autofillSetup + } + + func setAccountSetupVaultUnlock(_ vaultUnlockSetup: AccountSetupProgress?, userId: String?) async throws { + let userId = try unwrapUserId(userId) + accountSetupVaultUnlock[userId] = vaultUnlockSetup + } + func setActiveAccount(userId: String) async throws { guard let accounts, let match = accounts.first(where: { account in @@ -440,11 +456,6 @@ class MockStateService: StateService { // swiftlint:disable:this type_body_lengt masterPasswordHashes[userId] = hash } - func setNeedsVaultUnlockSetup(_ needsVaultUnlockSetup: Bool, userId: String?) async throws { - let userId = try unwrapUserId(userId) - self.needsVaultUnlockSetup[userId] = needsVaultUnlockSetup - } - func setNotificationsLastRegistrationDate(_ date: Date?, userId: String?) async throws { let userId = try unwrapUserId(userId) notificationsLastRegistrationDates[userId] = date diff --git a/BitwardenShared/UI/Auth/AuthCoordinator.swift b/BitwardenShared/UI/Auth/AuthCoordinator.swift index 88233a588..c92fcd714 100644 --- a/BitwardenShared/UI/Auth/AuthCoordinator.swift +++ b/BitwardenShared/UI/Auth/AuthCoordinator.swift @@ -177,8 +177,8 @@ final class AuthCoordinator: NSObject, // swiftlint:disable:this type_body_lengt showLanding() case let .landingSoftLoggedOut(email): showLanding(email: email) - case let .login(username): - showLogin(username) + case let .login(username, isNewAccount): + showLogin(username, isNewAccount: isNewAccount) case let .showLoginDecryptionOptions(organizationIdentifier): showLoginDecryptionOptions(organizationIdentifier) case let .loginWithDevice(email, type, isAuthenticated): @@ -465,9 +465,11 @@ final class AuthCoordinator: NSObject, // swiftlint:disable:this type_body_lengt /// Shows the login screen. If the create account flow is being presented it will be dismissed /// and the login screen will be pushed /// - /// - Parameter username: The user's username. + /// - Parameters: + /// - username: The user's username. + /// - isNewAccount: Whether the user is logging into a newly created account. /// - private func showLogin(_ username: String) { + private func showLogin(_ username: String, isNewAccount: Bool) { guard let stackNavigator else { return } let isPresenting = stackNavigator.rootViewController?.presentedViewController != nil @@ -476,6 +478,7 @@ final class AuthCoordinator: NSObject, // swiftlint:disable:this type_body_lengt ) let state = LoginState( + isNewAccount: isNewAccount, serverURLString: environmentUrls.webVaultURL.host ?? "", username: username ) @@ -816,7 +819,7 @@ final class AuthCoordinator: NSObject, // swiftlint:disable:this type_body_lengt vaultUnlockSetupHelper: DefaultVaultUnlockSetupHelper(services: services) ) let view = VaultUnlockSetupView(store: Store(processor: processor)) - stackNavigator?.push(view) + stackNavigator?.replace(view) } /// Show the WebAuthn two factor authentication view. diff --git a/BitwardenShared/UI/Auth/AuthCoordinatorTests.swift b/BitwardenShared/UI/Auth/AuthCoordinatorTests.swift index 018d82c88..acb09ffd5 100644 --- a/BitwardenShared/UI/Auth/AuthCoordinatorTests.swift +++ b/BitwardenShared/UI/Auth/AuthCoordinatorTests.swift @@ -299,6 +299,25 @@ class AuthCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this type_b XCTAssertEqual(state.serverURLString, "vault.bitwarden.eu") } + /// `navigate(to:)` with `.login` pushes the login view onto the stack navigator and hides the back button. + @MainActor + func test_navigate_login_newAccount() throws { + appSettingsStore.preAuthEnvironmentUrls = EnvironmentUrlData.defaultEU + subject.navigate(to: .login(username: "username", isNewAccount: true)) + + XCTAssertEqual(stackNavigator.actions.last?.type, .pushed) + let viewController = try XCTUnwrap( + stackNavigator.actions.last?.view as? UIHostingController + ) + XCTAssertTrue(viewController.navigationItem.hidesBackButton) + + let view = viewController.rootView + let state = view.store.state + XCTAssertTrue(state.isNewAccount) + XCTAssertEqual(state.username, "username") + XCTAssertEqual(state.serverURLString, "vault.bitwarden.eu") + } + /// `navigate(to:)` with `.login`, when using a self-hosted environment, /// pushes the login view onto the stack navigator and hides the back button. /// It also initializes `LoginState` with the self-hosted URL host. @@ -506,7 +525,7 @@ class AuthCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this type_b func test_navigate_vaultUnlockSetup() throws { subject.navigate(to: .vaultUnlockSetup) - XCTAssertEqual(stackNavigator.actions.last?.type, .pushed) + XCTAssertEqual(stackNavigator.actions.last?.type, .replaced) XCTAssertTrue(stackNavigator.actions.last?.view is VaultUnlockSetupView) } diff --git a/BitwardenShared/UI/Auth/AuthRoute.swift b/BitwardenShared/UI/Auth/AuthRoute.swift index faada7e19..3c48f6084 100644 --- a/BitwardenShared/UI/Auth/AuthRoute.swift +++ b/BitwardenShared/UI/Auth/AuthRoute.swift @@ -78,9 +78,11 @@ public enum AuthRoute: Equatable { /// A route to the login screen. /// - /// - Parameter username: The username to display on the login screen. + /// - Parameters: + /// - username: The username to display on the login screen. + /// - isNewAccount: Whether the user is logging into a newly created account. /// - case login(username: String) + case login(username: String, isNewAccount: Bool = false) /// A route to the login decryption options screen. /// diff --git a/BitwardenShared/UI/Auth/AuthRouterTests.swift b/BitwardenShared/UI/Auth/AuthRouterTests.swift index e7327cb1f..05414969b 100644 --- a/BitwardenShared/UI/Auth/AuthRouterTests.swift +++ b/BitwardenShared/UI/Auth/AuthRouterTests.swift @@ -217,6 +217,27 @@ final class AuthRouterTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertEqual(route, .complete) } + /// `handleAndRoute(_:)` redirects `.didCompleteAuth` to `.autofillSetup` if the user still + /// needs to set up autofill. + func test_handleAndRoute_didCompleteAuth_incompleteAutofill() async { + // TODO: PM-10278 Add autofill setup screen +// authRepository.activeAccount = .fixture() +// stateService.activeAccount = .fixture() +// stateService.accountSetupAutofill["1"] = .incomplete +// let route = await subject.handleAndRoute(.didCompleteAuth) +// XCTAssertEqual(route, .autofillSetup) + } + + /// `handleAndRoute(_:)` redirects `.didCompleteAuth` to `.vaultUnlockSetup` if the user still + /// needs to set up a vault unlock method. + func test_handleAndRoute_didCompleteAuth_incompleteVaultSetup() async { + authRepository.activeAccount = .fixture() + stateService.activeAccount = .fixture() + stateService.accountSetupVaultUnlock["1"] = .incomplete + let route = await subject.handleAndRoute(.didCompleteAuth) + XCTAssertEqual(route, .vaultUnlockSetup) + } + /// `handleAndRoute(_ :)` redirects `.didCompleteAuth` to `.landing` when there are no accounts. func test_handleAndRoute_didCompleteAuth_noAccounts() async { let route = await subject.handleAndRoute(.didCompleteAuth) diff --git a/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessor.swift b/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessor.swift index 678d11a4c..863728eea 100644 --- a/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessor.swift +++ b/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessor.swift @@ -222,12 +222,14 @@ class CompleteRegistrationProcessor: StateProcessor< try await services.authService.loginWithMasterPassword( state.passwordText, username: state.userEmail, - captchaToken: captchaToken + captchaToken: captchaToken, + isNewAccount: true ) try await services.authRepository.unlockVaultWithPassword(password: state.passwordText) await coordinator.handleEvent(.didCompleteAuth) + coordinator.navigate(to: .dismiss) } catch let error as CompleteRegistrationError { showCompleteRegistrationErrorAlert(error) } catch { @@ -235,7 +237,7 @@ class CompleteRegistrationProcessor: StateProcessor< // If an error occurs after the account was created, dismiss the view and navigate // the user to the login screen to complete login. coordinator.navigate(to: .dismissWithAction(DismissAction { - self.coordinator.navigate(to: .login(username: self.state.userEmail)) + self.coordinator.navigate(to: .login(username: self.state.userEmail, isNewAccount: true)) self.coordinator.showToast(Localizations.accountSuccessfullyCreated) })) return diff --git a/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessorTests.swift b/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessorTests.swift index 7e6ccba4e..dfe3152a4 100644 --- a/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessorTests.swift +++ b/BitwardenShared/UI/Auth/CompleteRegistration/CompleteRegistrationProcessorTests.swift @@ -537,7 +537,7 @@ class CompleteRegistrationProcessorTests: BitwardenTestCase { } dismissAction?.action() XCTAssertEqual(coordinator.routes.count, 2) - XCTAssertEqual(coordinator.routes[1], .login(username: "email@example.com")) + XCTAssertEqual(coordinator.routes[1], .login(username: "email@example.com", isNewAccount: true)) XCTAssertEqual(coordinator.toastsShown, [Localizations.accountSuccessfullyCreated]) } @@ -568,7 +568,7 @@ class CompleteRegistrationProcessorTests: BitwardenTestCase { } dismissAction?.action() XCTAssertEqual(coordinator.routes.count, 2) - XCTAssertEqual(coordinator.routes[1], .login(username: "email@example.com")) + XCTAssertEqual(coordinator.routes[1], .login(username: "email@example.com", isNewAccount: true)) XCTAssertEqual(coordinator.toastsShown, [Localizations.accountSuccessfullyCreated]) } @@ -658,6 +658,7 @@ class CompleteRegistrationProcessorTests: BitwardenTestCase { XCTAssertEqual(authService.loginWithMasterPasswordPassword, "password1234") XCTAssertNil(authService.loginWithMasterPasswordCaptchaToken) XCTAssertEqual(authService.loginWithMasterPasswordUsername, "email@example.com") + XCTAssertTrue(authService.loginWithMasterPasswordIsNewAccount) XCTAssertEqual(client.requests.count, 2) XCTAssertEqual(client.requests[0].url, URL(string: "https://example.com/identity/accounts/register/finish")) diff --git a/BitwardenShared/UI/Auth/Extensions/AuthRouter+Redirects.swift b/BitwardenShared/UI/Auth/Extensions/AuthRouter+Redirects.swift index 427a35fd3..bb54bcd5c 100644 --- a/BitwardenShared/UI/Auth/Extensions/AuthRouter+Redirects.swift +++ b/BitwardenShared/UI/Auth/Extensions/AuthRouter+Redirects.swift @@ -1,5 +1,7 @@ // MARK: AuthRouterRedirects +// swiftlint:disable file_length + extension AuthRouter { /// Configures the app with an active account. /// @@ -30,6 +32,11 @@ extension AuthRouter { } if account.profile.forcePasswordResetReason != nil { return .updateMasterPassword + } else if await (try? services.stateService.getAccountSetupVaultUnlock()) == .incomplete { + return .vaultUnlockSetup + // TODO: PM-10278 Add autofill setup screen +// } else if await (try? services.stateService.getAccountSetupAutofill()) == .incomplete { +// return .autofillSetup } else { await setCarouselShownIfEnabled() return .complete diff --git a/BitwardenShared/UI/Auth/Login/LoginProcessor.swift b/BitwardenShared/UI/Auth/Login/LoginProcessor.swift index 40d3102cb..b3899983b 100644 --- a/BitwardenShared/UI/Auth/Login/LoginProcessor.swift +++ b/BitwardenShared/UI/Auth/Login/LoginProcessor.swift @@ -140,7 +140,8 @@ class LoginProcessor: StateProcessor { try await services.authService.loginWithMasterPassword( state.masterPassword, username: state.username, - captchaToken: captchaToken + captchaToken: captchaToken, + isNewAccount: state.isNewAccount ) // Unlock the vault. diff --git a/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift b/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift index a4a55349e..00ac8efbb 100644 --- a/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift +++ b/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift @@ -6,7 +6,7 @@ import XCTest // MARK: - LoginProcessorTests -class LoginProcessorTests: BitwardenTestCase { +class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_body_length // MARK: Properties var appSettingsStore: MockAppSettingsStore! @@ -165,6 +165,32 @@ class LoginProcessorTests: BitwardenTestCase { XCTAssertEqual(authService.loginWithMasterPasswordUsername, "email@example.com") XCTAssertEqual(authService.loginWithMasterPasswordPassword, "Password1234!") + XCTAssertFalse(authService.loginWithMasterPasswordIsNewAccount) + XCTAssertNil(authService.loginWithMasterPasswordCaptchaToken) + + XCTAssertEqual(coordinator.events.last, .didCompleteAuth) + XCTAssertFalse(coordinator.isLoadingOverlayShowing) + XCTAssertEqual(coordinator.loadingOverlaysShown, [.init(title: Localizations.loggingIn)]) + + XCTAssertEqual(authRepository.unlockVaultPassword, "Password1234!") + } + + /// `perform(_:)` with `.loginWithMasterPasswordPressed` logs the user in with the provided + /// master password for a newly created account. + @MainActor + func test_perform_loginWithMasterPasswordPressed_success_isNewAccount() async throws { + subject.state.isNewAccount = true + subject.state.username = "email@example.com" + subject.state.masterPassword = "Password1234!" + + authRepository.unlockWithPasswordResult = .success(()) + authRepository.activeAccount = .fixture() + + await subject.perform(.loginWithMasterPasswordPressed) + + XCTAssertEqual(authService.loginWithMasterPasswordUsername, "email@example.com") + XCTAssertEqual(authService.loginWithMasterPasswordPassword, "Password1234!") + XCTAssertTrue(authService.loginWithMasterPasswordIsNewAccount) XCTAssertNil(authService.loginWithMasterPasswordCaptchaToken) XCTAssertEqual(coordinator.events.last, .didCompleteAuth) diff --git a/BitwardenShared/UI/Auth/Login/LoginState.swift b/BitwardenShared/UI/Auth/Login/LoginState.swift index 4853ff605..1fba52d10 100644 --- a/BitwardenShared/UI/Auth/Login/LoginState.swift +++ b/BitwardenShared/UI/Auth/Login/LoginState.swift @@ -14,6 +14,9 @@ struct LoginState: Equatable { /// A flag indicating if the login with device button should be displayed or not. var isLoginWithDeviceVisible: Bool = false + /// Whether the user is logging into a newly created account. + var isNewAccount = false + /// The password visibility icon used in the view's text field. var passwordVisibleIcon: ImageAsset { isMasterPasswordRevealed ? Asset.Images.hidden : Asset.Images.visible diff --git a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessor.swift b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessor.swift index a385000ac..41b532ddc 100644 --- a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessor.swift +++ b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessor.swift @@ -71,11 +71,11 @@ class VaultUnlockSetupProcessor: StateProcessor