Skip to content

Commit

Permalink
[PM-14983] Support Optic ID and any future biometric authentication t…
Browse files Browse the repository at this point in the history
…ypes (#1146)
  • Loading branch information
bunnyhero authored Jan 29, 2025
1 parent 9981c9b commit c746146
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
4 changes: 4 additions & 0 deletions BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ struct VaultUnlockSetupState: Equatable {
"FaceID"
case .touchID:
"TouchID"
case .opticID:
"OpticID"
case .unknown:
"biometrics"
}
case .pin:
"PIN"
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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$@**?";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down

0 comments on commit c746146

Please sign in to comment.