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>
63 lines
2.2 KiB
Swift
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))
|
|
}
|
|
}
|