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 2 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 Touch ID.
Copy link
Contributor

@KatherineInCode KatherineInCode Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐ŸŽจ Can you please update these comments to indicate Optic ID and a generic biometrics fallback?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed comments in ecbdb5d.

@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_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