diff --git a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift index e9cbb05d84..4fbf4c7794 100644 --- a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift +++ b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift @@ -6,6 +6,12 @@ enum BiometricAuthenticationType: Equatable { /// FaceID biometric authentication. case faceID + /// OpticID biometric authentication. + case opticID + /// TouchID biometric authentication. case touchID + + /// Unknown other biometric authentication + case unknown } diff --git a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsService.swift b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsService.swift index ae4f60f3cf..13f9738ca5 100644 --- a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsService.swift +++ b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsService.swift @@ -85,15 +85,16 @@ class DefaultBiometricsService: BiometricsService { } switch authContext.biometryType { - case .none, - .opticID: + case .faceID: + return .faceID + case .none: return .none + case .opticID: + return .opticID case .touchID: return .touchID - case .faceID: - return .faceID @unknown default: - return .none + return .unknown } } diff --git a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift index 31365b8d52..bd6fc50dac 100644 --- a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift +++ b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift @@ -193,8 +193,12 @@ struct VaultUnlockView: View { switch biometryType { case .faceID: Text(Localizations.useFaceIDToUnlock) + case .opticID: + Text(Localizations.useOpticIDToUnlock) case .touchID: Text(Localizations.useFingerprintToUnlock) + case .unknown: + Text(Localizations.useBiometricsToUnlock) } } } diff --git a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockViewTests.swift b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockViewTests.swift index 3ac3f4e97f..bf6f17c8c8 100644 --- a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockViewTests.swift +++ b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockViewTests.swift @@ -118,6 +118,20 @@ class VaultUnlockViewTests: BitwardenTestCase { ) expectedString = Localizations.useFingerprintToUnlock button = try subject.inspect().find(button: expectedString) + + processor.state.biometricUnlockStatus = .available( + .opticID, + enabled: true + ) + expectedString = Localizations.useOpticIDToUnlock + button = try subject.inspect().find(button: expectedString) + + processor.state.biometricUnlockStatus = .available( + .unknown, + enabled: true + ) + expectedString = Localizations.useBiometricsToUnlock + button = try subject.inspect().find(button: expectedString) try button.tap() waitFor(!processor.effects.isEmpty) XCTAssertEqual(processor.effects.last, .unlockVaultWithBiometrics) diff --git a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessorTests.swift b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessorTests.swift index e343bdecc5..0ba944915a 100644 --- a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessorTests.swift +++ b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessorTests.swift @@ -93,6 +93,18 @@ class VaultUnlockSetupProcessorTests: BitwardenTestCase { XCTAssertEqual(subject.state.unlockMethods, [.biometrics(.faceID), .pin]) } + /// `perform(_:)` with `.loadData` fetches the biometrics unlock status for a device with generic biometrics. + @MainActor + func test_perform_loadData_biometrics() async { + let status = BiometricsUnlockStatus.available(.unknown, enabled: false) + biometricsRepository.biometricUnlockStatus = .success(status) + + await subject.perform(.loadData) + + XCTAssertEqual(subject.state.biometricsStatus, status) + XCTAssertEqual(subject.state.unlockMethods, [.biometrics(.unknown), .pin]) + } + /// `perform(_:)` with `.loadData` logs the error and shows an alert if one occurs. @MainActor func test_perform_loadData_error() async { @@ -117,6 +129,18 @@ class VaultUnlockSetupProcessorTests: BitwardenTestCase { XCTAssertEqual(subject.state.unlockMethods, [.pin]) } + /// `perform(_:)` with `.loadData` fetches the biometrics unlock status for a device with Optic ID. + @MainActor + func test_perform_loadData_opticID() async { + let status = BiometricsUnlockStatus.available(.opticID, enabled: false) + biometricsRepository.biometricUnlockStatus = .success(status) + + await subject.perform(.loadData) + + XCTAssertEqual(subject.state.biometricsStatus, status) + XCTAssertEqual(subject.state.unlockMethods, [.biometrics(.opticID), .pin]) + } + /// `perform(_:)` with `.loadData` fetches the biometrics unlock status for a device with Touch ID. @MainActor func test_perform_loadData_touchID() async { diff --git a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupState.swift b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupState.swift index ecabfd27c4..9b5d6a440c 100644 --- a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupState.swift +++ b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupState.swift @@ -43,6 +43,10 @@ struct VaultUnlockSetupState: Equatable { "FaceID" case .touchID: "TouchID" + case .opticID: + "OpticID" + case .unknown: + "biometrics" } case .pin: "PIN" @@ -56,8 +60,12 @@ struct VaultUnlockSetupState: Equatable { switch type { case .faceID: Localizations.unlockWith(Localizations.faceID) + case .opticID: + Localizations.unlockWith(Localizations.opticID) case .touchID: Localizations.unlockWith(Localizations.touchID) + case .unknown: + Localizations.unlockWithUnknownBiometrics } case .pin: Localizations.unlockWithPIN diff --git a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings index a2c4e83408..47a7fca406 100644 --- a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings +++ b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings @@ -182,6 +182,7 @@ "TwoStepLogin" = "Two-step login"; "UnlockWith" = "Unlock with %1$@"; "UnlockWithPIN" = "Unlock with PIN code"; +"UnlockWithUnknownBiometrics" = "Unlock with biometrics"; "Validating" = "Validating"; "VerificationCode" = "Verification code"; "ViewItem" = "View item"; @@ -456,6 +457,7 @@ "ExitConfirmation" = "Are you sure you want to exit Bitwarden?"; "PINRequireMasterPasswordRestart" = "Do you want to require unlocking with your master password when the application is restarted?"; "PINRequireBioOrMasterPasswordRestart" = "Do you want to require unlocking with %1$@ or your master password when the application is restarted?"; +"PINRequireUnknownBiometricsOrMasterPasswordRestart" = "Do you want to require unlocking with biometrics or your master password when the application is restarted?"; "Black" = "Black"; "Nord" = "Nord"; "SolarizedDark" = "Solarized Dark"; @@ -1059,6 +1061,8 @@ "CopyPrivateKey" = "Copy private key"; "CopyFingerprint" = "Copy fingerprint"; "SSHKeys" = "SSH keys"; +"OpticID" = "Optic ID"; +"UseOpticIDToUnlock" = "Use Optic ID To Unlock"; "ImportantNotice" = "Important notice"; "BitwardenWillSendACodeToYourAccountEmailDescriptionLong" = "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."; "DoYouHaveReliableAccessToYourEmail" = "Do you have reliable access to your email, **%1$@**?"; diff --git a/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift b/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift index 26ca101c66..5c2c66bf75 100644 --- a/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift +++ b/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift @@ -249,8 +249,12 @@ extension Alert { let message = switch biometricType { case .faceID: Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.faceID) + case .opticID: + Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.opticID) case .touchID: Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.touchID) + case .unknown: + Localizations.pinRequireUnknownBiometricsOrMasterPasswordRestart case nil: Localizations.pinRequireMasterPasswordRestart } diff --git a/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift b/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift index 4c2b0386b8..c417b08207 100644 --- a/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift +++ b/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift @@ -200,6 +200,17 @@ class AlertSettingsTests: BitwardenTestCase { XCTAssertEqual(subject.message, Localizations.pinRequireMasterPasswordRestart) } + /// `unlockWithPINCodeAlert(action)` constructs an `Alert` with the correct title, message, Yes and No buttons + /// when `biometricType` is `biometrics`. + func test_unlockWithPINAlert_biometrics() { + let subject = Alert.unlockWithPINCodeAlert(biometricType: .unknown) { _ in } + + XCTAssertEqual(subject.alertActions.count, 2) + XCTAssertEqual(subject.preferredStyle, .alert) + XCTAssertEqual(subject.title, Localizations.unlockWithPIN) + XCTAssertEqual(subject.message, Localizations.pinRequireUnknownBiometricsOrMasterPasswordRestart) + } + /// `unlockWithPINCodeAlert(action)` constructs an `Alert` with the correct title, message, Yes and No buttons /// when `biometricType` is `faceID`. func test_unlockWithPINAlert_faceID() { @@ -211,6 +222,17 @@ class AlertSettingsTests: BitwardenTestCase { XCTAssertEqual(subject.message, Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.faceID)) } + /// `unlockWithPINCodeAlert(action)` constructs an `Alert` with the correct title, message, Yes and No buttons + /// when `biometricType` is `opticID`. + func test_unlockWithPINAlert_opticID() { + let subject = Alert.unlockWithPINCodeAlert(biometricType: .opticID) { _ in } + + XCTAssertEqual(subject.alertActions.count, 2) + XCTAssertEqual(subject.preferredStyle, .alert) + XCTAssertEqual(subject.title, Localizations.unlockWithPIN) + XCTAssertEqual(subject.message, Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.opticID)) + } + /// `unlockWithPINCodeAlert(action)` constructs an `Alert` with the correct title, message, Yes and No buttons /// when `biometricType` is `touchID`. func test_unlockWithPINAlert_touchID() { diff --git a/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift b/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift index 1aca31a101..dcff73e929 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift @@ -258,8 +258,12 @@ struct AccountSecurityView: View { switch biometryType { case .faceID: return Localizations.unlockWith(Localizations.faceID) + case .opticID: + return Localizations.unlockWith(Localizations.opticID) case .touchID: return Localizations.unlockWith(Localizations.touchID) + case .unknown: + return Localizations.unlockWithUnknownBiometrics } } }