Files
scan-receipts/app/ios/ScanReceipts/Features/Settings/DeleteAccountView.swift

122 lines
4.7 KiB
Swift

import SwiftUI
/// Permanent account deletion. Requires typing the account's email exactly
/// (compared against `appState.currentUser?.email`) plus the password before
/// the destructive action becomes enabled, then a second confirmation
/// dialog before the call actually fires.
struct DeleteAccountView: View {
@EnvironmentObject private var appState: AppState
@State private var confirmEmail = ""
@State private var password = ""
@State private var isSubmitting = false
@State private var errorMessage: String?
@State private var showConfirmDialog = false
// TODO: `Models/User.swift` doesn't currently expose whether an account
// is Google-only (i.e. has no password to check).
// `AccountAPI.deleteAccount(password:confirmEmail:)` accepts `password:
// nil` specifically for that case, but there's no signal here to detect
// it, so this screen always shows and requires the password field. Once
// the `User` model exposes something like `hasPassword`, make this field
// optional/hidden for Google-only accounts and pass `password: nil`
// instead of always requiring input here.
private var emailMatches: Bool {
guard let accountEmail = appState.currentUser?.email, !accountEmail.isEmpty else { return false }
return confirmEmail == accountEmail
}
private var canSubmit: Bool {
emailMatches && !password.isEmpty && !isSubmitting
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: ZenithSpacing.md) {
Text("Diese Aktion ist unwiderruflich. Dein Konto sowie alle Belege und Ordner werden dauerhaft gelöscht.")
.zenithBodyStyle(color: .zenithError)
ZenithDivider()
VStack(alignment: .leading, spacing: ZenithSpacing.sm) {
Text("Bestätigung").zenithLabelCapsStyle()
if let email = appState.currentUser?.email {
Text("Gib zur Bestätigung deine E-Mail-Adresse ein: \(email)")
.zenithBodySmStyle()
}
ZenithTextField(
label: "E-Mail-Adresse",
text: $confirmEmail,
placeholder: "E-Mail-Adresse",
keyboardType: .emailAddress,
textContentType: .emailAddress,
autocapitalization: .never,
autocorrectionDisabled: true
)
ZenithTextField(
label: "Passwort",
text: $password,
placeholder: "Passwort",
isSecure: true,
textContentType: .password
)
}
if let errorMessage {
Text(errorMessage)
.zenithBodySmStyle(color: .zenithError)
}
Button(role: .destructive) {
showConfirmDialog = true
} label: {
if isSubmitting {
ProgressView()
.tint(.white)
} else {
Text("Konto endgültig löschen")
}
}
.buttonStyle(.zenithPrimaryDestructive)
.disabled(!canSubmit)
.padding(.top, ZenithSpacing.sm)
}
.padding(ZenithSpacing.sm)
}
.background(Color.zenithBg)
.navigationTitle("Konto löschen")
.navigationBarTitleDisplayMode(.inline)
.confirmationDialog(
"Konto wirklich endgültig löschen?",
isPresented: $showConfirmDialog,
titleVisibility: .visible
) {
Button("Endgültig löschen", role: .destructive) {
Task { await deleteAccount() }
}
Button("Abbrechen", role: .cancel) {}
} message: {
Text("Diese Aktion kann nicht rückgängig gemacht werden.")
}
}
private func deleteAccount() async {
errorMessage = nil
isSubmitting = true
defer { isSubmitting = false }
do {
try await AccountAPI.deleteAccount(password: password, confirmEmail: confirmEmail)
// The account and its session are already gone server-side
// this just resets local state. RootView (App/RootView.swift)
// switches to the login screen automatically once
// AppState.phase flips to .signedOut.
await appState.logout()
} catch {
errorMessage = error.localizedDescription
}
}
}