import ManaAuthUI import ManaCore import SwiftData import SwiftUI struct AccountView: View { @Environment(AuthClient.self) private var auth @Environment(ManaAuthGate.self) private var authGate @Environment(\.modelContext) private var context @State private var showChangeEmail = false @State private var showChangePassword = false @State private var showDeleteAccount = false var body: some View { ZStack { WordeckTheme.background.ignoresSafeArea() Group { switch auth.status { case .signedIn: signedInContent case .guest, .signedOut, .error, .unknown: guestContent case .signingIn, .twoFactorRequired: ProgressView().tint(WordeckTheme.primary) } } } .navigationTitle("Account") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .manaBrand(WordeckBrand.manaBrand) .sheet(isPresented: $showChangeEmail) { ManaChangeEmailView( auth: auth, callbackUniversalLink: URL(string: "https://wordeck.com/auth/email-changed"), onDone: { showChangeEmail = false } ) .manaBrand(WordeckBrand.manaBrand) } .sheet(isPresented: $showChangePassword) { ManaChangePasswordView( auth: auth, onDone: { showChangePassword = false } ) .manaBrand(WordeckBrand.manaBrand) } .sheet(isPresented: $showDeleteAccount) { ManaDeleteAccountView( auth: auth, onDone: { Task { await wipeLocalCache() } showDeleteAccount = false } ) .manaBrand(WordeckBrand.manaBrand) } } private var signedInContent: some View { VStack(spacing: 20) { Image(systemName: "person.crop.circle.fill") .resizable() .frame(width: 80, height: 80) .foregroundStyle(WordeckTheme.primary) if let email = auth.currentEmail { Text(email) .font(.headline) .foregroundStyle(WordeckTheme.foreground) } VStack(spacing: 12) { NavigationLink { SettingsView() } label: { rowLabel("Einstellungen", systemImage: "gear") } .buttonStyle(.plain) Button { showChangeEmail = true } label: { rowLabel("Email ändern", systemImage: "envelope") } .buttonStyle(.plain) Button { showChangePassword = true } label: { rowLabel("Passwort ändern", systemImage: "key") } .buttonStyle(.plain) ManaTwoFactorAccountRow(auth: auth) .padding(.vertical, 12) .padding(.horizontal, 16) .background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(WordeckTheme.border, lineWidth: 1) ) } .padding(.horizontal, 32) Spacer() Button(role: .destructive) { // Logout behält die Guest-Identity → App bleibt im // anonymen Modus nutzbar (lokale Decks, Marketplace // browsen). Wer „alles vergessen" will, nutzt // „Account löschen". // // DSGVO: Cache (Karten + Due-Reviews + Decks + // pending Grades) wird vor dem signOut gewipet, damit // ein anderer User auf demselben Gerät keine Daten // des Vorgängers sieht. Task { await wipeLocalCache() await auth.signOut(keepGuestMode: true) } } label: { Text("Abmelden") .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(WordeckTheme.error.opacity(0.1), in: RoundedRectangle(cornerRadius: 8)) .foregroundStyle(WordeckTheme.error) } .padding(.horizontal, 32) // App-Store-Guideline 5.1.1(v): jede App mit Sign-Up MUSS // eine Account-Löschung anbieten. Button(role: .destructive) { showDeleteAccount = true } label: { Text("Account löschen…") .font(.footnote) .foregroundStyle(WordeckTheme.mutedForeground) } .padding(.bottom, 16) } .padding(.top, 48) } private var guestContent: some View { VStack(spacing: 20) { Image(systemName: "person.crop.circle.dashed") .resizable() .frame(width: 80, height: 80) .foregroundStyle(WordeckTheme.mutedForeground) VStack(spacing: 8) { Text("Du nutzt Wordeck anonym") .font(.headline) .foregroundStyle(WordeckTheme.foreground) Text( """ Marketplace und lokale Decks funktionieren ohne Konto. \ Für KI-Karten, eigene Decks im Cloud-Sync und Marketplace-\ Veröffentlichung brauchst du ein Konto. """ ) .font(.subheadline) .foregroundStyle(WordeckTheme.mutedForeground) .multilineTextAlignment(.center) } .padding(.horizontal, 32) VStack(spacing: 12) { Button { // Trigger ohne pending-Action — wir wollen einfach // das Sign-In-Sheet öffnen. `require` mit no-op // schaltet die Sheet-Logik des Gates ein. authGate.require(reason: "account-tab") {} } label: { Text("Anmelden / Konto erstellen") .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(WordeckTheme.primary, in: RoundedRectangle(cornerRadius: 10)) .foregroundStyle(.white) } .buttonStyle(.plain) NavigationLink { SettingsView() } label: { rowLabel("Einstellungen", systemImage: "gear") } .buttonStyle(.plain) } .padding(.horizontal, 32) Spacer() } .padding(.top, 48) } /// Löscht alle lokal gecachten User-Daten: Decks, Karten, fällige /// Reviews und die offline Grade-Queue. Wird vor jedem signOut und /// vor Account-Löschung aufgerufen. private func wipeLocalCache() async { try? context.delete(model: CachedDeck.self) try? context.delete(model: CachedCard.self) try? context.delete(model: CachedDueReview.self) try? context.delete(model: PendingGrade.self) try? context.save() Log.app.info("Local cache wiped (signOut / delete-account)") } private func rowLabel(_ title: String, systemImage: String) -> some View { Label(title, systemImage: systemImage) .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 12) .padding(.horizontal, 16) .background(WordeckTheme.surface, in: RoundedRectangle(cornerRadius: 8)) .foregroundStyle(WordeckTheme.foreground) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(WordeckTheme.border, lineWidth: 1) ) } } #Preview { NavigationStack { AccountView() .environment(AuthClient(config: AppConfig.manaAppConfig)) } }