mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-10 16:27:59 -04:00
191 lines
6.1 KiB
Swift
191 lines
6.1 KiB
Swift
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 = ""
|
|
}
|
|
}
|
|
}
|