diff --git a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift index e9cbb05d8..0eca4eba1 100644 --- a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift +++ b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricAuthenticationType.swift @@ -8,4 +8,10 @@ enum BiometricAuthenticationType: Equatable { /// TouchID biometric authentication. case touchID + + /// OpticID biometric authentication. + case opticID + + /// Unknown other biometric authentication + case biometrics } diff --git a/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsService.swift b/BitwardenShared/Core/Auth/Services/Biometrics/BiometricsService.swift index ae4f60f3c..0f411981b 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 .none: return .none case .touchID: return .touchID case .faceID: return .faceID + case .opticID: + return .opticID @unknown default: - return .none + return .biometrics } } diff --git a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift index 0c4d550d3..85ffaeade 100644 --- a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift +++ b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift @@ -192,6 +192,10 @@ struct VaultUnlockView: View { Text(Localizations.useFaceIDToUnlock) case .touchID: Text(Localizations.useFingerprintToUnlock) + case .opticID: + Text(Localizations.useOpticIDToUnlock) + case .biometrics: + Text(Localizations.useBiometricsToUnlock) } } } diff --git a/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockViewTests.swift b/BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockViewTests.swift index a4d54253f..b3268d46d 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( + .biometrics, + 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 e343bdecc..b77a47791 100644 --- a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessorTests.swift +++ b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupProcessorTests.swift @@ -129,6 +129,30 @@ class VaultUnlockSetupProcessorTests: BitwardenTestCase { XCTAssertEqual(subject.state.unlockMethods, [.biometrics(.touchID), .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 generic biometrics. + @MainActor + func test_perform_loadData_biometrics() async { + let status = BiometricsUnlockStatus.available(.biometrics, enabled: false) + biometricsRepository.biometricUnlockStatus = .success(status) + + await subject.perform(.loadData) + + XCTAssertEqual(subject.state.biometricsStatus, status) + XCTAssertEqual(subject.state.unlockMethods, [.biometrics(.biometrics), .pin]) + } + /// `perform(_:)` with `.toggleUnlockMethod` disables biometrics unlock and updates the state. @MainActor func test_perform_toggleUnlockMethod_biometrics_disable() async { diff --git a/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupState.swift b/BitwardenShared/UI/Auth/VaultUnlockSetup/VaultUnlockSetupState.swift index ecabfd27c..7da018896 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 .biometrics: + "Biometrics" } case .pin: "PIN" @@ -58,6 +62,10 @@ struct VaultUnlockSetupState: Equatable { Localizations.unlockWith(Localizations.faceID) case .touchID: Localizations.unlockWith(Localizations.touchID) + case .opticID: + Localizations.unlockWith(Localizations.opticID) + case .biometrics: + Localizations.unlockWith(Localizations.biometrics) } 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 dd447656b..566dc6e4b 100644 --- a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings +++ b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings @@ -1059,3 +1059,5 @@ "CopyPrivateKey" = "Copy private key"; "CopyFingerprint" = "Copy fingerprint"; "SSHKeys" = "SSH keys"; +"OpticID" = "Optic ID"; +"UseOpticIDToUnlock" = "Use Optic ID To Unlock"; diff --git a/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift b/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift index 26ca101c6..207de1a29 100644 --- a/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift +++ b/BitwardenShared/UI/Platform/Settings/Extensions/Alert+Settings.swift @@ -251,6 +251,10 @@ extension Alert { Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.faceID) case .touchID: Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.touchID) + case .opticID: + Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.opticID) + case .biometrics: + Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.biometrics) case nil: Localizations.pinRequireMasterPasswordRestart } diff --git a/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift b/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift index 4c2b0386b..370e1927f 100644 --- a/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift +++ b/BitwardenShared/UI/Platform/Settings/Extensions/AlertSettingsTests.swift @@ -222,6 +222,28 @@ class AlertSettingsTests: BitwardenTestCase { XCTAssertEqual(subject.message, Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.touchID)) } + /// `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 `biometrics`. + func test_unlockWithPINAlert_biometrics() { + let subject = Alert.unlockWithPINCodeAlert(biometricType: .biometrics) { _ in } + + XCTAssertEqual(subject.alertActions.count, 2) + XCTAssertEqual(subject.preferredStyle, .alert) + XCTAssertEqual(subject.title, Localizations.unlockWithPIN) + XCTAssertEqual(subject.message, Localizations.pinRequireBioOrMasterPasswordRestart(Localizations.biometrics)) + } + /// `verificationCodePrompt(completion:)` constructs an `Alert` used to ask the user to entered /// the verification code that was sent to their email. /// diff --git a/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift b/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift index aea38c0b9..ea60b6a93 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityView.swift @@ -270,6 +270,10 @@ struct AccountSecurityView: View { return Localizations.unlockWith(Localizations.faceID) case .touchID: return Localizations.unlockWith(Localizations.touchID) + case .opticID: + return Localizations.unlockWith(Localizations.opticID) + case .biometrics: + return Localizations.unlockWith(Localizations.biometrics) } } }