import Foundation import ManaCore import Observation /// State-Maschine für ``ManaTwoFactorChallengeView``. Setzt auf den /// `.twoFactorRequired`-Zustand des `AuthClient` auf, der nach einem /// erfolgreichen Email/PW-`signIn` mit 2FA-aktiviertem Account /// gesetzt wird. @MainActor @Observable public final class TwoFactorChallengeViewModel { public enum Mode: Equatable, Sendable { case totp case backupCode } public enum Status: Equatable, Sendable { case idle case verifying case error(String) } public var mode: Mode = .totp public var code: String = "" public var trustDevice: Bool = false public private(set) var status: Status = .idle private let auth: AuthClient public init(auth: AuthClient) { self.auth = auth } public var canSubmit: Bool { guard !code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return false } if case .verifying = status { return false } switch mode { case .totp: // TOTP: 6 Ziffern (Better-Auth-Default) let digitsOnly = code.filter { $0.isNumber } return digitsOnly.count == 6 case .backupCode: // Backup-Codes: ~10 Zeichen alphanumerisch + Trenner. // Pragmatik: nicht-leer reicht — Server validiert exakt. return !code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } public var isVerifying: Bool { if case .verifying = status { return true } return false } public func toggleMode() { mode = mode == .totp ? .backupCode : .totp code = "" status = .idle } public func submit() async { let cleaned = code.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { return } status = .verifying do { switch mode { case .totp: try await auth.verifyTotp(code: cleaned, trustDevice: trustDevice) case .backupCode: try await auth.verifyBackupCode(code: cleaned, trustDevice: trustDevice) } // Bei Erfolg: Status bleibt .verifying — die View beobachtet // den AuthClient.status (.signedIn) und reagiert über den // umgebenden Gate/Root-View. Code aus dem Memory wischen. code = "" status = .idle } catch let error as AuthError { status = .error(error.errorDescription ?? "Verifikation fehlgeschlagen") } catch { status = .error(String(describing: error)) } } }