mana-swift-ui/Sources/ManaAuthUI/Account/ManaAccountView.swift
Till JS 0b276fe903 feat(ManaAccountView): onDeleted-Callback nach Konto-Löschung
Optionaler async-Callback, der nach erfolgreicher Account-Löschung läuft —
für App-lokalen Cleanup (z.B. wordeck wischt SwiftData-Cache, DSGVO:
anderer User am selben Gerät).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:34:40 +02:00

179 lines
6.3 KiB
Swift

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 onDeleted: (() 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,
onDeleted: (() async -> Void)? = nil,
@ViewBuilder extra: @escaping () -> Extra = { EmptyView() }
) {
self.auth = auth
self.config = config
self.onSignIn = onSignIn
self.onSignOut = onSignOut ?? { await auth.signOut() }
self.onDeleted = onDeleted
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
}
// App-spezifische Sektionen erscheinen in BEIDEN Zuständen
// soft-auth-Apps (pageta u.a.) brauchen ihre Settings auch
// ausgeloggt sichtbar.
extra()
}
.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
if let onDeleted { Task { await onDeleted() } }
})
}
}
@ViewBuilder
private var signedInSections: some View {
Section("Konto") {
if let email = auth.currentEmail {
LabeledContent("E-Mail", value: email)
}
}
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)
}
}
}
}