8000 PM-10276: Set up unlock: enable pin unlock by matt-livefront · Pull Request #867 · bitwarden/ios · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

PM-10276: Set up unlock: enable pin unlock #867

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

Merged
merged 5 commits into from
Sep 13, 2024
Merged
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 @@ -49,6 +49,7 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
var setMasterPasswordOrganizationIdentifier: String?
var setMasterPasswordResetPasswordAutoEnroll: Bool?
var setMasterPasswordResult: Result<Void, Error> = .success(())
var setPinsRequirePasswordAfterRestart: Bool?
var setPinsResult: Result<Void, Error> = .success(())
var setVaultTimeoutError: Error?
var unlockVaultFromLoginWithDeviceKey: String?
Expand Down Expand Up @@ -219,9 +220,10 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
return match
}

func setPins(_ pin: String, requirePasswordAfterRestart _: Bool) async throws {
func setPins(_ pin: String, requirePasswordAfterRestart: Bool) async throws {
encryptedPin = pin
pinProtectedUserKey = pin
setPinsRequirePasswordAfterRestart = requirePasswordAfterRestart
try setPinsResult.get()
}

Expand Down
3 changes: 2 additions & 1 deletion BitwardenShared/UI/Auth/AuthCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,8 @@ final class AuthCoordinator: NSObject, // swiftlint:disable:this type_body_lengt
let processor = VaultUnlockSetupProcessor(
coordinator: asAnyCoordinator(),
services: services,
state: VaultUnlockSetupState()
state: VaultUnlockSetupState(),
vaultUnlockSetupHelper: DefaultVaultUnlockSetupHelper(services: services)
)
let view = VaultUnlockSetupView(store: Store(processor: processor))
stackNavigator?.push(view)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@testable import BitwardenShared

class MockVaultUnlockSetupHelper: VaultUnlockSetupHelper {
var setBiometricUnlockCalled = false
var setBiometricUnlockStatus: BiometricsUnlockStatus?

var setPinUnlockCalled = false
var setPinUnlockResult: Bool?

func setBiometricUnlock(enabled: Bool, showAlert: @escaping (Alert) -> Void) async -> BiometricsUnlockStatus? {
setBiometricUnlockCalled = true
return setBiometricUnlockStatus
}

func setPinUnlock(enabled: Bool, showAlert: @escaping (Alert) -> Void) async -> Bool {
setPinUnlockCalled = true
return setPinUnlockResult ?? !enabled
}
}
153 changes: 153 additions & 0 deletions BitwardenShared/UI/Auth/Utilities/VaultUnlockSetupHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// MARK: - VaultUnlockSetupHelper

/// A protocol for a helper used to set up vault unlock methods.
///
protocol VaultUnlockSetupHelper: AnyObject {
/// Enables or disables biometric vault unlock.
///
/// - Parameters:
/// - enabled: Whether to enable biometric vault unlock.
/// - showAlert: A closure used to handle showing an alert if needed while toggling biometric
/// vault unlock.
/// - Returns: The biometric unlock status after vault unlock has been updated.
///
func setBiometricUnlock(enabled: Bool, showAlert: @escaping (Alert) -> Void) async -> BiometricsUnlockStatus?

/// Enables or disables pin vault unlock.
///
/// - Parameters:
/// - enabled: Whether to enable pin vault unlock.
/// - showAlert: A closure used to handle showing an alert if needed while toggling pin vault unlock.
/// - Returns: Whether pin unlock is enabled after vault unlock has been updated.
///
func setPinUnlock(enabled: Bool, showAlert: @escaping (Alert) -> Void) async -> Bool
}

// MARK: - VaultUnlockSetupHelperError

/// Errors thrown by a `VaultUnlockSetupHelper`.
///
enum VaultUnlockSetupHelperError: Error {
/// The user cancelled setting up an unlock method.
case userCancelled
}

// MARK: - DefaultVaultUnlockSetupHelper

/// A default implementation of `VaultUnlockSetupHelper` which is used to set up vault unlock methods.
///
@MainActor
class DefaultVaultUnlockSetupHelper {
Copy link
Collaborator

Choose a reason for hiding this comment

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

just curious, why not make this a service itself?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We've used this concept of a helper elsewhere to extract logic from the processor or share logic between multiple processors. In this case, while not directly showing alerts, it does end up creating alerts that should be shown. So I feel like it belongs in the UI layer rather than the service layer.

// MARK: Types

typealias Services = HasAuthRepository
& HasBiometricsRepository
& HasErrorReporter
& HasStateService

// MARK: Properties

/// The services used by this helper.
private let services: Services

// MARK: Initialization

/// Creates a new `DefaultVaultUnlockSetupHelper`.
///
/// - Parameters:
/// - services: The services used by this helper.
///
init(services: Services) {
self.services = services
}

// MARK: Private

/// Shows an alert for the user to enter their pin for vault unlock.
///
/// - Parameter showAlert: A closure used to handling showing the alert.
/// - Returns: The pin that the user entered.
///
private func showEnterPinAlert(showAlert: @escaping (Alert) -> Void) async throws -> String {
try await withCheckedThrowingContinuation { continuation in
showAlert(.enterPINCode(
onCancelled: {
continuation.resume(throwing: VaultUnlockSetupHelperError.userCancelled)
},
completion: { pin in
continuation.resume(returning: pin)
}
))
}
}

/// Shows an alert asking the user whether they want to require their master password to unlock
/// the vault when the app restarts.
///
/// - Parameters:
/// - biometricType: The biometric type the app supports.
/// - showAlert: A closure used to handle showing the alert.
/// - Returns: Whether the user wants to require their master password on app restart.
///
private func showUnlockWithPinAlert(
biometricType: BiometricAuthenticationType?,
showAlert: @escaping (Alert) -> Void
) async -> Bool {
await withCheckedContinuation { continuation in
showAlert(.unlockWithPINCodeAlert(biometricType: biometricType) { requirePassword in
continuation.resume(returning: requirePassword)
})
}
}
}

// MARK: - DefaultVaultUnlockSetupHelper+VaultUnlockSetupHelper

extension DefaultVaultUnlockSetupHelper: VaultUnlockSetupHelper {
func setBiometricUnlock(enabled: Bool, showAlert: @escaping (Alert) -> Void) async -> BiometricsUnlockStatus? {
do {
try await services.authRepository.allowBioMetricUnlock(enabled)
return try await services.biometricsRepository.getBiometricUnlockStatus()
} catch {
services.errorReporter.log(error: error)
showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))

// In the case of an error, still attempt to return the unlock status. This status may
// be used by the UI to determine whether biometrics are available on the device.
return try? await services.biometricsRepository.getBiometricUnlockStatus()
}
}

func setPinUnlock(enabled: Bool, showAlert: @escaping (Alert) -> Void) async -> Bool {
do {
guard enabled else {
try await services.authRepository.clearPins()
return false
}

let pin = try await showEnterPinAlert(showAlert: showAlert)
guard !pin.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return false }

let userHasMasterPassword = try await services.stateService.getUserHasMasterPassword()
let biometricType = services.biometricsRepository.getBiometricAuthenticationType()
let requirePasswordAfterRestart = if userHasMasterPassword {
await showUnlockWithPinAlert(biometricType: biometricType, showAlert: showAlert)
} else {
false
}

try await services.authRepository.setPins(
pin,
requirePasswordAfterRestart: requirePasswordAfterRestart
)

return true
} catch VaultUnlockSetupHelperError.userCancelled {
return !enabled
} catch {
services.errorReporter.log(error: error)
showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
return false
}
}
}
Loading
Loading
0