feat(ManaAuthUI): ManaAccountView — vereinheitlichtes Konto-Gerüst

Form-basierter Standard-Block (E-Mail/Passwort ändern, 2FA, Konto
löschen, Abmelden) + Sign-In-CTA, Config-Flags, onSignIn/onSignOut,
extra-Slot für App-Sektionen. Beendet das per-App-Handbauen des
Konto-Tabs (Pendant zur Login-Konsolidierung). swift build grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-28 11:35:26 +02:00
parent 9844759e86
commit 4a77b57c80
2 changed files with 180 additions and 0 deletions

View file

@ -8,6 +8,15 @@ Alle Änderungen werden hier dokumentiert. Format orientiert an
### Hinzugefügt
- **`ManaAccountView`** (ManaAuthUI) — vereinheitlichtes Konto-Gerüst:
Form-basierter Standard-Block (E-Mail-Anzeige, E-Mail/Passwort ändern,
2FA, Konto löschen [Apple 5.1.1(v)], Abmelden) aus einem `AuthClient`,
plus Sign-In-CTA im abgemeldeten Zustand. Config-Flags
(`showChangeEmail`/`showChangePassword`/`showTwoFactor`),
`onSignIn`/`onSignOut`-Callbacks, `@ViewBuilder extra`-Slot für
App-spezifische Sektionen. Pendant zur Login-Konsolidierung — beendet
das per-App-Handbauen des Konto-Tabs.
- **`ManaFeedbackUI`** — neues Library-Produkt. `ManaFeedbackSheet`
(Kind-Picker Wunsch/Problem/Feedback, Titel, Beschreibung; bei anonymer
Nutzung optionales Kontakt-Mail-Feld + Moderations-Hinweis),

View file

@ -0,0 +1,171 @@
import ManaCore
import SwiftUI
/// Vereinheitlichtes Konto-Gerüst für alle Verein-Apps. Rendert den
/// **Standard-Block** der Konto-Verwaltung E-Mail-Anzeige, E-Mail ändern,
/// Passwort ändern, 2FA, Konto löschen (Apple 5.1.1(v)), Abmelden aus einem
/// `AuthClient`, plus einen Sign-In-CTA im abgemeldeten Zustand. Pendant zur
/// Login-Konsolidierung: nach `ManaLoginView` ist auch der Konto-Tab überall
/// identisch, statt pro App handgebaut.
///
/// App-spezifische Inhalte (Profil-Cards, Beitrags-Historie, Settings-Link,
/// Health-Status) kommen als `Section`s in den `extra`-Slot sie erscheinen
/// im eingeloggten Zustand zwischen Konto-Info und Sicherheits-Block.
///
/// Theming über `\.manaBrand` (App setzt es auf den View; vererbt sich auf die
/// Verwaltungs-Sheets). Login-Auslösung bleibt App-Sache (`onSignIn` typisch
/// `ManaAuthGate.require`), damit die App ihren Gate-/SSO-Flow behält.
///
/// ```swift
/// NavigationStack {
/// ManaAccountView(
/// auth: auth,
/// onSignIn: { authGate.require(reason: "konto") {} }
/// ) {
/// Section("Profil") { app-spezifisch }
/// }
/// .navigationTitle("Konto")
/// }
/// ```
public struct ManaAccountView<Extra: View>: View {
/// Welche Verwaltungs-Aktionen der Standard-Block zeigt. Apps ohne
/// Passwort-Auth (reine SSO-/Read-Apps) können E-Mail/Passwort/2FA
/// abschalten Konto-Löschung + Abmelden bleiben immer.
public struct Config: Sendable {
public var showChangeEmail: Bool
public var showChangePassword: Bool
public var showTwoFactor: Bool
/// Universal-Link, den die E-Mail-Änderungs-Bestätigung anspringt.
public var changeEmailCallbackLink: URL?
public var signInLabel: String
public var signedOutMessage: String?
public init(
showChangeEmail: Bool = true,
showChangePassword: Bool = true,
showTwoFactor: Bool = true,
changeEmailCallbackLink: URL? = nil,
signInLabel: String = "Mit mana-Konto anmelden",
signedOutMessage: String? = nil
) {
self.showChangeEmail = showChangeEmail
self.showChangePassword = showChangePassword
self.showTwoFactor = showTwoFactor
self.changeEmailCallbackLink = changeEmailCallbackLink
self.signInLabel = signInLabel
self.signedOutMessage = signedOutMessage
}
}
private let auth: AuthClient
private let config: Config
private let onSignIn: () -> Void
private let onSignOut: () async -> Void
private let extra: () -> Extra
@State private var showChangeEmail = false
@State private var showChangePassword = false
@State private var showDeleteAccount = false
/// - Parameters:
/// - auth: der App-`AuthClient`.
/// - config: welche Verwaltungs-Aktionen erscheinen (Default: alle).
/// - onSignIn: Login auslösen (typisch `ManaAuthGate.require`).
/// - onSignOut: Abmelden + App-Cleanup. Default: `auth.signOut()`.
/// - extra: App-spezifische `Section`s (nur eingeloggt sichtbar).
public init(
auth: AuthClient,
config: Config = .init(),
onSignIn: @escaping () -> Void,
onSignOut: (() async -> Void)? = nil,
@ViewBuilder extra: @escaping () -> Extra = { EmptyView() }
) {
self.auth = auth
self.config = config
self.onSignIn = onSignIn
self.onSignOut = onSignOut ?? { await auth.signOut() }
self.extra = extra
}
private var isSignedIn: Bool {
if case .signedIn = auth.status { return true }
return false
}
private var showsSecuritySection: Bool {
config.showChangeEmail || config.showChangePassword || config.showTwoFactor
}
public var body: some View {
Form {
if isSignedIn {
signedInSections
} else {
signedOutSection
}
}
.sheet(isPresented: $showChangeEmail) {
ManaChangeEmailView(
auth: auth,
callbackUniversalLink: config.changeEmailCallbackLink,
onDone: { showChangeEmail = false }
)
}
.sheet(isPresented: $showChangePassword) {
ManaChangePasswordView(auth: auth, onDone: { showChangePassword = false })
}
.sheet(isPresented: $showDeleteAccount) {
ManaDeleteAccountView(auth: auth, onDone: { showDeleteAccount = false })
}
}
@ViewBuilder
private var signedInSections: some View {
Section("Konto") {
if let email = auth.currentEmail {
LabeledContent("E-Mail", value: email)
}
}
extra()
if showsSecuritySection {
Section("Sicherheit") {
if config.showChangeEmail {
Button("E-Mail ändern") { showChangeEmail = true }
}
if config.showChangePassword {
Button("Passwort ändern") { showChangePassword = true }
}
if config.showTwoFactor {
ManaTwoFactorAccountRow(auth: auth)
}
}
}
Section {
Button("Abmelden", role: .destructive) {
Task { await onSignOut() }
}
// Apple-Guideline 5.1.1(v): In-App-Account-Löschung Pflicht.
Button("Konto löschen…", role: .destructive) {
showDeleteAccount = true
}
}
}
@ViewBuilder
private var signedOutSection: some View {
Section {
Button {
onSignIn()
} label: {
Label(config.signInLabel, systemImage: "person.crop.circle.badge.checkmark")
}
} footer: {
if let message = config.signedOutMessage {
Text(message)
}
}
}
}