Apps mit Guest-/Login-optional-Modus brauchen kein Full-Screen-
Login-Gate mehr. Aktionen, die einen Account brauchen, werden in
gate.require { ... } gewrappt — Login wird zur Inline-Eskalation,
nicht zum App-Block.
ManaAuthGate — @Observable-State-Maschine:
- require(reason:work:) sync und async Overloads
- Bei .signedIn: Aktion läuft sofort
- Bei .signedOut/.guest: Sign-In-Sheet öffnet, Aktion gemerkt;
nach erfolgreichem Sign-In läuft sie automatisch
- lastReason als optionaler Telemetrie-Hint
ManaAuthGateModifier / View.manaAuthGate(_:signIn:) — hängt das
Sign-In-Sheet an einen Root-View, beobachtet auth.status. Wechsel
auf .signedIn schließt das Sheet + resolved pending; manuelles
Dismiss verwirft.
Konvention für native Apps:
1. bootstrap() → bei .signedOut → enterGuestMode()
2. Root-View zeigt immer App-Inhalte, nie eine Vollbild-Login-Wall
3. Account-Aktionen via gate.require { ... }
Memoro hat das informell schon. Cards komplett umgestellt (siehe
cards-native commit). Manaspur folgt.
7 neue Tests: sofortiger Run bei .signedIn, Defer bei .signedOut/
.guest, resolvePending, cancelPending, lastReason-Tracking.
Setzt mana-swift-core ≥ 1.2.0 voraus.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
62 lines
2.2 KiB
Swift
62 lines
2.2 KiB
Swift
import ManaCore
|
|
import SwiftUI
|
|
|
|
/// ViewModifier, der das Sign-In-Sheet für einen ``ManaAuthGate``
|
|
/// präsentiert. Die App liefert via `signIn`-Builder, welche Auth-
|
|
/// View im Sheet gezeigt wird (üblich: ``ManaLoginView``).
|
|
///
|
|
/// Beobachtet `gate.auth.status` und schließt das Sheet automatisch
|
|
/// sobald der User eingeloggt ist; die ausstehende Aktion wird dann
|
|
/// gestartet. Wird das Sheet vorher dismisst, wird die Aktion verworfen.
|
|
public struct ManaAuthGateModifier<SignInContent: View>: ViewModifier {
|
|
@Bindable private var gate: ManaAuthGate
|
|
private let signInContent: () -> SignInContent
|
|
|
|
public init(gate: ManaAuthGate, @ViewBuilder signIn: @escaping () -> SignInContent) {
|
|
self.gate = gate
|
|
signInContent = signIn
|
|
}
|
|
|
|
public func body(content: Content) -> some View {
|
|
content
|
|
.sheet(
|
|
isPresented: $gate.isPresentingSignIn,
|
|
onDismiss: { gate.cancelPending() }
|
|
) {
|
|
signInContent()
|
|
.onChange(of: authStatusKey(gate.auth.status)) { _, _ in
|
|
gate.resolvePending()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reduziert `AuthClient.Status` auf einen `Equatable`-stabilen Key
|
|
/// für `onChange(of:)`. `Status` selbst ist `Equatable` — aber assoziierte
|
|
/// Werte (Email-String) sind hier irrelevant, wir wollen nur auf den
|
|
/// Übergang signedOut/guest → signedIn reagieren.
|
|
private func authStatusKey(_ status: AuthClient.Status) -> Int {
|
|
switch status {
|
|
case .unknown: 0
|
|
case .signedOut: 1
|
|
case .guest: 2
|
|
case .signingIn: 3
|
|
case .signedIn: 4
|
|
case .error: 5
|
|
}
|
|
}
|
|
|
|
public extension View {
|
|
/// Hängt einen ``ManaAuthGate`` an die View. Aufrufe von
|
|
/// `gate.require { ... }` führen entweder zur Aktion (signedIn) oder
|
|
/// zum Sign-In-Sheet, das der `signIn`-Builder liefert.
|
|
///
|
|
/// Idiomatischer Einsatz: einmal am App-Root, danach den Gate via
|
|
/// `.environment(gate)` in den Sub-Views verfügbar machen.
|
|
func manaAuthGate(
|
|
_ gate: ManaAuthGate,
|
|
@ViewBuilder signIn: @escaping () -> some View
|
|
) -> some View {
|
|
modifier(ManaAuthGateModifier(gate: gate, signIn: signIn))
|
|
}
|
|
}
|