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:
parent
9844759e86
commit
4a77b57c80
2 changed files with 180 additions and 0 deletions
|
|
@ -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),
|
||||
|
|
|
|||
171
Sources/ManaAuthUI/Account/ManaAccountView.swift
Normal file
171
Sources/ManaAuthUI/Account/ManaAccountView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue