mana-swift-ui/Sources/ManaAuthUI/Gate/ManaAuthGateModifier.swift
Till JS c1555565b6 v0.3.0 — ManaTwoFactorChallengeView
Apps mit aktivem 2FA bekommen jetzt eine native Challenge-View nach
Email/Password-Login. ManaLoginView schaltet automatisch um wenn
AuthClient.status auf .twoFactorRequired wechselt.

Components:
- ManaTwoFactorChallengeView — Scaffold-View mit 6-stelligem Code-
  Input, Backup-Code-Toggle, Cancel zurück zum Login
- TwoFactorChallengeViewModel — @Observable State-Maschine, wraps
  AuthClient.verifyTotp/verifyBackupCode
- LoginViewModel.Status.twoFactorRequired(email:) als neuer Case;
  submit() routet automatisch dorthin wenn der AuthClient den
  Challenge-Status zurückgibt

6 neue Tests, 39/39 grün.

Setzt mana-swift-core ≥ 1.3.0 voraus.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:20:30 +02:00

63 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 .twoFactorRequired: 4
case .signedIn: 5
case .error: 6
}
}
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))
}
}