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-14983] Support Optic ID and any future biometric authentication types #1146

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ enum BiometricAuthenticationType: Equatable {

/// TouchID biometric authentication.
case touchID

/// OpticID biometric authentication.
case opticID

/// Unknown other biometric authentication
case biometrics
}
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 .none:
return .none
case .touchID:
return .touchID
case .faceID:
return .faceID
case .opticID:
return .opticID
@unknown default:
return .none
return .biometrics
}
}

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 @@ -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)
}
}
}
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(
.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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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 .biometrics:
"Biometrics"
}
case .pin:
"PIN"
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1059,3 +1059,5 @@
"CopyPrivateKey" = "Copy private key";
"CopyFingerprint" = "Copy fingerprint";
"SSHKeys" = "SSH keys";
"OpticID" = "Optic ID";
"UseOpticIDToUnlock" = "Use Optic ID To Unlock";
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Loading