import Foundation import SwiftUI import UIKit private let locBundle = Bundle.vaultUI /// Configuration for PIN setup flow public struct PinSetupConfiguration { /// Current step in the setup process public enum Step { case enterNew case confirm } public let step: Step public let title: String public let subtitle: String public let pinLength: Int? // nil = variable length (4-8 digits) public let firstStepPin: String? // Pin from first step (used in confirm step) public init(step: Step, title: String, subtitle: String, pinLength: Int?, firstStepPin: String? = nil) { self.step = step self.title = title self.subtitle = subtitle self.pinLength = pinLength self.firstStepPin = firstStepPin } } /// ViewModel for PIN setup flow @MainActor public class PinSetupViewModel: ObservableObject { @Published public var pin: String = "" @Published public var error: String? @Published public var isProcessing: Bool = false @Published public var configuration: PinSetupConfiguration private let setupHandler: (String) async throws -> Void private let cancelHandler: () -> Void public init( setupHandler: @escaping (String) async throws -> Void, cancelHandler: @escaping () -> Void ) { self.setupHandler = setupHandler self.cancelHandler = cancelHandler // Start with first step self.configuration = PinSetupConfiguration( step: .enterNew, title: String(localized: "pin_setup_title", bundle: locBundle), subtitle: String(localized: "pin_setup_subtitle", bundle: locBundle), pinLength: nil // Variable length for first step ) } public func addDigit(_ digit: String) { // Clear error when user starts typing again error = nil // Check if we've reached max length let maxLength = configuration.pinLength ?? 8 // Max 8 digits in setup mode if pin.count >= maxLength { return } pin += digit // Auto-submit when PIN reaches expected length (only for confirm step with fixed length) if configuration.step == .confirm, let expectedLength = configuration.pinLength, pin.count == expectedLength { // Small delay to show the last dot filled before submitting Task { try? await Task.sleep(nanoseconds: 100_000_000) // 100ms await submitPin() } } } public func removeDigit() { guard !pin.isEmpty else { return } pin.removeLast() error = nil } public func cancel() { cancelHandler() } public func submitPin() async { // Validate minimum length for setup mode if configuration.step == .enterNew && pin.count < 4 { return // Don't submit until at least 4 digits } isProcessing = true // Give UI time to update try? await Task.sleep(nanoseconds: 50_000_000) // 50ms switch configuration.step { case .enterNew: // Move to confirm step let pinLength = pin.count let firstStepPin = pin isProcessing = false // Update configuration for confirm step configuration = PinSetupConfiguration( step: .confirm, title: String(localized: "pin_confirm_title", bundle: locBundle), subtitle: String(localized: "pin_confirm_subtitle", bundle: locBundle), pinLength: pinLength, // Fix length for confirmation firstStepPin: firstStepPin ) // Clear current PIN for confirmation entry pin = "" case .confirm: // Check if PINs match guard let firstPin = configuration.firstStepPin else { isProcessing = false self.error = String(localized: "unknown_error", bundle: locBundle) triggerErrorFeedback() return } if pin != firstPin { // PINs don't match - restart from beginning isProcessing = false self.error = String(localized: "pin_mismatch", bundle: locBundle) triggerErrorFeedback() // Wait to let user see the error message try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second // Restart from beginning configuration = PinSetupConfiguration( step: .enterNew, title: String(localized: "pin_setup_title", bundle: locBundle), subtitle: String(localized: "pin_setup_subtitle", bundle: locBundle), pinLength: nil ) pin = "" error = nil return } // PINs match - setup the PIN do { try await setupHandler(pin) // Success - the handler will navigate away or complete the flow // Keep loading state active since we're navigating } catch { // Generic error fallback isProcessing = false self.error = String(localized: "unknown_error", bundle: locBundle) triggerErrorFeedback() shakeAndClear() } } } /// Check if we can submit based on current state public var canSubmit: Bool { switch configuration.step { case .enterNew: return pin.count >= 4 && pin.count <= 8 case .confirm: return pin.count == configuration.pinLength } } 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 { try? await Task.sleep(nanoseconds: 500_000_000) // 500ms pin = "" } } }