import ManaCore import Observation /// Action-Level-Gate für Apps mit Guest-/Login-optional-Modus. /// /// Hintergrund: in `mana-swift-core` v1.2.0 wurde `AuthClient.Status` /// um den anonymen `.guest`-Modus erweitert. Apps sollen ihre Inhalte /// in diesem Modus möglichst vollständig zugänglich machen — nur /// schreibende Server-Aktionen (KI-Generierung, Publish, Cross-Device- /// Sync) sollen Login erzwingen. /// /// `ManaAuthGate` wrappt diese Logik so, dass sie aus Buttons heraus /// einzeilig nutzbar bleibt: /// /// ```swift /// @Environment(ManaAuthGate.self) private var gate /// /// Button("Karte mit KI generieren") { /// gate.require { /// // läuft erst, wenn .signedIn — sonst poppt das Login-Sheet, /// // und die Aktion läuft nach erfolgreichem Sign-In. /// await viewModel.generateCard() /// } /// } /// ``` /// /// In der App-Root wird der Gate einmal aufgebaut und via Modifier /// `manaAuthGate(_:signIn:)` an die View gehängt: /// /// ```swift /// RootView() /// .manaAuthGate(authGate) { /// ManaLoginView( /// auth: authClient, /// onSignUpTapped: { /* push SignUp */ }, /// onForgotTapped: { /* push Forgot */ } /// ) /// } /// .environment(authGate) /// ``` /// /// Verhalten: /// - Status ist `.signedIn` → Action läuft sofort. /// - Status ist `.guest`/`.signedOut`/`.error`/`.unknown` → Sign-In- /// Sheet wird präsentiert, die Action gemerkt; nach Wechsel auf /// `.signedIn` läuft die Action und das Sheet wird geschlossen. /// - User schließt das Sheet ohne Login → Action wird verworfen. @MainActor @Observable public final class ManaAuthGate { public let auth: AuthClient public var isPresentingSignIn: Bool = false /// Letzter aufgetretener Trigger-Grund. Diagnostik/Telemetrie: Apps /// können hier ablesen, *was* den User in den Login-Flow geführt /// hat (z.B. "KI-Generierung" vs. "Deck publish"), ohne dass das /// in der Action selbst stehen muss. public private(set) var lastReason: String? private var pending: PendingAction? public init(auth: AuthClient) { self.auth = auth } /// Führt `action` sofort aus, wenn der User eingeloggt ist. Sonst /// wird das Sign-In-Sheet aufgeklappt und die Action wird ausgeführt, /// sobald `auth.status` auf `.signedIn` wechselt. /// /// - Parameters: /// - reason: Optional, kurzer technischer Kennzeichner für /// Telemetrie/Logs. Wird der UI **nicht** angezeigt. /// - action: Die zu schützende Aktion. Wird **nicht** ausgeführt, /// wenn der User das Sheet ohne Login schließt. public func require(reason: String? = nil, _ action: @escaping @MainActor () -> Void) { lastReason = reason if case .signedIn = auth.status { action() return } pending = .sync(action) isPresentingSignIn = true } /// Async-Variante von ``require(reason:_:)``. Die Action darf /// `await`-Aufrufe enthalten; sie wird in einem neuen `Task` /// gestartet. public func require( reason: String? = nil, _ action: @escaping @MainActor () async -> Void ) { lastReason = reason if case .signedIn = auth.status { Task { await action() } return } pending = .async(action) isPresentingSignIn = true } /// Wechselt der Auth-Status auf `.signedIn`, läuft die ausstehende /// Action und das Sheet wird geschlossen. Vom ViewModifier /// aufgerufen, kann aber auch manuell genutzt werden. public func resolvePending() { guard case .signedIn = auth.status else { return } let next = pending pending = nil isPresentingSignIn = false switch next { case .none: return case let .sync(action): action() case let .async(action): Task { await action() } } } /// Verwirft eine ausstehende Aktion. Vom ViewModifier nach /// `sheet(onDismiss:)` aufgerufen — wenn der User das Sheet ohne /// Login schließt, soll die Aktion *nicht* nachträglich laufen. public func cancelPending() { pending = nil } private enum PendingAction { case sync(@MainActor () -> Void) case async(@MainActor () async -> Void) } }