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>
171 lines
5.9 KiB
Swift
171 lines
5.9 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 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)
|
|
}
|
|
}
|
|
}
|
|
}
|