Tweak iOS native pin unlock view flow (#1340)

This commit is contained in:
Leendert de Borst
2025-11-11 22:54:39 +01:00
committed by Leendert de Borst
parent e5ed8d380f
commit 7b6170e927
7 changed files with 83 additions and 13 deletions

View File

@@ -268,6 +268,7 @@ extension CredentialProviderViewController: PasskeyProviderDelegate {
)
},
cancelHandler: { [weak self] in
// Passkey registration - just cancel on any dismissal
self?.extensionContext.cancelRequest(withError: NSError(
domain: ASExtensionErrorDomain,
code: ASExtensionError.userCanceled.rawValue

View File

@@ -235,8 +235,36 @@ public class CredentialProviderViewController: ASCredentialProviderViewControlle
}
}
},
cancelHandler: { [weak self] in
self?.handleCancel()
cancelHandler: { [weak self] pinWasDisabled in
guard let self = self else { return }
if pinWasDisabled {
// PIN was disabled due to max attempts
// Try biometric as fallback if available
if vaultStore.isBiometricAuthEnabled() {
do {
// Try to unlock with biometric
try vaultStore.unlockVault()
// Success - process the credential request
if let passkeyRequest = self.quickReturnPasskeyRequest {
self.handleQuickReturnPasskeyCredential(vaultStore: vaultStore, request: passkeyRequest)
} else if let passwordRequest = self.quickReturnPasswordRequest {
self.handleQuickReturnPasswordCredential(vaultStore: vaultStore, request: passwordRequest)
}
} catch {
// Biometric failed - cancel
print("Biometric fallback failed after PIN lockout: \(error)")
self.handleCancel()
}
} else {
// No other auth method - cancel
self.handleCancel()
}
} else {
// User manually cancelled
self.handleCancel()
}
}
)
@@ -525,8 +553,8 @@ public class CredentialProviderViewController: ASCredentialProviderViewControlle
// Check if PIN is available as fallback
if !pinEnabled {
let alert = UIAlertController(
title: String(format: NSLocalizedString("biometric_required", comment: ""), authMethod),
message: String(format: NSLocalizedString("biometric_required_message", comment: ""), authMethod),
title: String(format: NSLocalizedString("auth_required", comment: ""), authMethod),
message: String(format: NSLocalizedString("auth_required_message", comment: ""), authMethod),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .default) { [weak self] _ in

View File

@@ -100,8 +100,24 @@ class UnlockCoordinator: ObservableObject {
self.onUnlocked()
}
},
cancelHandler: { [weak self] in
self?.cancel()
cancelHandler: { [weak self] pinWasDisabled in
guard let self = self else { return }
if pinWasDisabled {
// PIN was disabled due to max attempts
// Try biometric as fallback if available
if self.vaultStore.isBiometricAuthEnabled() {
Task {
await self.attemptBiometricUnlock()
}
} else {
// No other auth method available - cancel
self.cancel()
}
} else {
// User manually cancelled - just cancel
self.cancel()
}
}
)
}

View File

Binary file not shown.

View File

@@ -859,10 +859,16 @@ public class VaultManager: NSObject {
}
}
},
cancelHandler: {
// User cancelled
cancelHandler: { pinWasDisabled in
// Dismiss the view
rootVC.dismiss(animated: true) {
reject("USER_CANCELLED", "User cancelled PIN unlock", nil)
if pinWasDisabled {
// PIN was disabled due to max attempts
reject("PIN_DISABLED", "PIN was disabled after too many failed attempts", nil)
} else {
// User manually cancelled
reject("USER_CANCELLED", "User cancelled PIN unlock", nil)
}
}
}
)

View File

@@ -1,4 +1,5 @@
import SwiftUI
import UIKit
private let locBundle = Bundle.vaultUI
@@ -288,12 +289,12 @@ public class PinUnlockViewModel: ObservableObject {
public let pinLength: Int?
private let unlockHandler: (String) async throws -> Void
private let cancelHandler: () -> Void
private let cancelHandler: (Bool) -> Void // Bool indicates if PIN was disabled/locked
public init(
pinLength: Int?,
unlockHandler: @escaping (String) async throws -> Void,
cancelHandler: @escaping () -> Void
cancelHandler: @escaping (Bool) -> Void
) {
self.pinLength = pinLength
self.unlockHandler = unlockHandler
@@ -328,7 +329,7 @@ public class PinUnlockViewModel: ObservableObject {
}
public func cancel() {
cancelHandler()
cancelHandler(false) // User manually cancelled
}
private func attemptUnlock() async {
@@ -341,18 +342,36 @@ public class PinUnlockViewModel: ObservableObject {
// Success - the handler will navigate away or complete the flow
// Keep loading state active since we're navigating
} catch let nsError as NSError {
// Handle unlock errors
// Check for PIN disabled errors (error code 25)
// This occurs when PIN was disabled due to max attempts or configuration issue
if nsError.code == 25 {
// PIN is no longer available - auto-dismiss the view
// This happens when user enters wrong PIN too many times
isUnlocking = false
cancelHandler(true)
return
}
// Handle other unlock errors (incorrect PIN, etc.)
isUnlocking = false
self.error = nsError.localizedDescription
triggerErrorFeedback()
shakeAndClear()
} catch let genericError {
// Generic error
isUnlocking = false
self.error = String(localized: "unlock_failed", bundle: locBundle)
triggerErrorFeedback()
shakeAndClear()
}
}
private func triggerErrorFeedback() {
// Trigger haptic feedback for error
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.error)
}
private func shakeAndClear() {
// Clear the PIN after a short delay to show error
Task {

View File

Binary file not shown.