mana-swift-ui/Sources/ManaAuthUI/Login/ManaLoginView.swift
Till JS 0a2cb349b4 v0.1.0 — initialer Sprint, vollständige Auth-Reise als SwiftUI
Phase 2 aus dem Native-Auth-Vollausbau-Plan (Option A, siehe
../mana/docs/MANA_SWIFT.md). Entstanden weil drei Apps fast-
byte-identische LoginView.swift hatten und Sign-Up/Forgot-PW
komplett fehlten.

ManaAuthUI-Library mit:
- ManaBrandConfig — App-injizierte Theme-Werte (forest für Cards/
  Manaspur, mana-default für Memoro), Environment-Key, View-Modifier
- Base-Components: ManaAuthScaffold, ManaPrimaryButton, ManaTextField,
  ManaSecureField + .manaEmailField()-Modifier
- ManaLoginView + LoginViewModel — Email/PW-Login, schaltet bei
  AuthError.emailNotVerified automatisch auf ManaEmailVerifyGateView
- ManaSignUpView + SignUpViewModel — Email/Name/PW + awaiting-
  Verification-Hinweis-Screen
- ManaEmailVerifyGateView + ViewModel — Resend-Verification
- ManaForgotPasswordView + ViewModel — Reset-Mail anfordern (immer
  generischer Hinweis, User-Enumeration-Schutz)
- ManaResetPasswordView + ViewModel — neues PW mit Token aus
  Universal-Link
- ManaChangeEmailView, ManaChangePasswordView, ManaDeleteAccountView
  + internal ViewModels — Account-Bausteine
- ManaDeleteAccountView ist zweistufig (Bestätigungs-Wort tippen
  + Passwort) → App-Store-Guideline 5.1.1(v) Pflicht-Surface

26/26 ViewModel-Tests grün via per-test-ID URLProtocol-Routing
(löst Parallel-Pollution zwischen .serialized Suites).

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:22:42 +02:00

95 lines
3 KiB
Swift

import ManaCore
import SwiftUI
/// Vollständiger Login-Screen: Email + Passwort + Primary-Submit
/// + Sekundär-Buttons "Konto erstellen" und "Passwort vergessen".
///
/// Bei `.emailNotVerified` schaltet die View automatisch auf
/// ``ManaEmailVerifyGateView`` um der User kann von dort die
/// Verify-Mail erneut anfordern.
///
/// Apps binden ein:
/// ```swift
/// ManaLoginView(
/// auth: authClient,
/// onSignUpTapped: { presentingSignUp = true },
/// onForgotTapped: { presentingForgot = true }
/// )
/// .manaBrand(.cardecky)
/// ```
public struct ManaLoginView: View {
@Environment(\.manaBrand) private var brand
@State private var model: LoginViewModel
private let auth: AuthClient
private let onSignUpTapped: () -> Void
private let onForgotTapped: () -> Void
/// - Parameters:
/// - auth: gemeinsamer `AuthClient` der App.
/// - onSignUpTapped: präsentiert ``ManaSignUpView`` (Sheet,
/// Push, Navigation die App entscheidet).
/// - onForgotTapped: präsentiert ``ManaForgotPasswordView``.
public init(
auth: AuthClient,
onSignUpTapped: @escaping () -> Void,
onForgotTapped: @escaping () -> Void
) {
self.auth = auth
_model = State(initialValue: LoginViewModel(auth: auth))
self.onSignUpTapped = onSignUpTapped
self.onForgotTapped = onForgotTapped
}
public var body: some View {
switch model.status {
case let .emailNotVerified(email):
ManaEmailVerifyGateView(
email: email,
auth: auth,
onBackToLogin: { model.resetToIdle() }
)
default:
loginForm
}
}
@ViewBuilder
private var loginForm: some View {
ManaAuthScaffold {
VStack(spacing: 16) {
ManaTextField("Email", text: $model.email)
.manaEmailField()
ManaSecureField("Passwort", text: $model.password, textContentType: .password)
ManaPrimaryButton(
"Anmelden",
isLoading: model.isSigningIn,
isEnabled: model.canSubmit
) {
Task { await model.submit() }
}
if case let .error(message) = model.status {
Text(message)
.font(.footnote)
.foregroundStyle(brand.error)
.multilineTextAlignment(.center)
.padding(.top, 4)
}
}
.padding(.top, 16)
VStack(spacing: 12) {
Button("Konto erstellen", action: onSignUpTapped)
.font(.subheadline)
.foregroundStyle(brand.primary)
Button("Passwort vergessen?", action: onForgotTapped)
.font(.subheadline)
.foregroundStyle(brand.mutedForeground)
}
.padding(.top, 8)
}
}
}