- PrivacyInfo.xcprivacy: NSPrivacyTracking=false, deklariert gesammelte Daten (Email, Name, User-ID, präziser/grober Standort, Fotos, Fitness, Sonstiger Nutzerinhalt) — alle linked to user, kein Tracking, nur AppFunctionality. Required-Reason APIs: UserDefaults (CA92.1), FileTimestamp (C617.1), DiskSpace (E174.1), SystemBootTime (35F9.1). - SettingsView: Section 'Konto löschen' mit zwei Bestätigungs-Alerts → DELETE /api/profile/account → automatischer logout. Erfüllt Apple- Pflicht seit iOS 16.4 (in-App-Löschung statt Web-Redirect). - Info.plist: CFBundleDevelopmentRegion explizit 'de' (statt der $(DEVELOPMENT_LANGUAGE)-Variable, die sonst 'en' auflöst) → Store zeigt App als deutschsprachig an. - NSHealthShareUsageDescription präziser formuliert (Reviewer-Hint).
280 lines
12 KiB
Swift
280 lines
12 KiB
Swift
import SwiftUI
|
|
|
|
struct SettingsView: View {
|
|
@Environment(AuthSession.self) private var auth
|
|
@AppStorage("autoPauseEnabled") private var autoPauseEnabled = true
|
|
@AppStorage("healthKitSyncEnabled") private var healthKitSyncEnabled = false
|
|
@State private var showHealthPermissionAlert = false
|
|
@State private var showDeleteConfirm1 = false
|
|
@State private var showDeleteConfirm2 = false
|
|
@State private var isDeleting = false
|
|
@State private var deleteError: String?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
HStack(spacing: 14) {
|
|
avatarView
|
|
.frame(width: 60, height: 60)
|
|
.clipShape(Circle())
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(displayName)
|
|
.font(.headline)
|
|
if let email = auth.profile?.email {
|
|
Text(email)
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
|
|
Section("Hund & Alltag") {
|
|
NavigationLink {
|
|
GassiView()
|
|
} label: {
|
|
Label("Gassi-Treffen", systemImage: "pawprint.fill")
|
|
}
|
|
NavigationLink {
|
|
GiftkoederView()
|
|
} label: {
|
|
Label("Giftköder", systemImage: "exclamationmark.octagon.fill")
|
|
}
|
|
NavigationLink {
|
|
VerloreneHundeView()
|
|
} label: {
|
|
Label("Verlorene Hunde", systemImage: "magnifyingglass.circle.fill")
|
|
}
|
|
NavigationLink {
|
|
AusgabenView()
|
|
} label: {
|
|
Label("Ausgaben", systemImage: "eurosign.circle.fill")
|
|
}
|
|
}
|
|
|
|
Section("Account") {
|
|
LabeledContent("Rolle", value: rolleLabel)
|
|
if auth.profile?.isFounderFlag == true {
|
|
LabeledContent("Founder", value: founderLabel)
|
|
}
|
|
if auth.profile?.isPartnerFlag == true {
|
|
LabeledContent("Partner", value: "Ja")
|
|
}
|
|
if let tier = auth.profile?.subscriptionTier, !tier.isEmpty {
|
|
LabeledContent("Abo", value: tier.capitalized)
|
|
}
|
|
LabeledContent("Premium", value: premiumValue ? "Ja" : "Nein")
|
|
}
|
|
|
|
if let wohnort = auth.profile?.wohnort, !wohnort.isEmpty {
|
|
Section("Ort") {
|
|
Text(wohnort)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Toggle(isOn: $autoPauseEnabled) {
|
|
Label("Auto-Pause", systemImage: "pause.circle")
|
|
}
|
|
Toggle(isOn: $healthKitSyncEnabled) {
|
|
Label("Apple Health Sync", systemImage: "heart.fill")
|
|
}
|
|
.onChange(of: healthKitSyncEnabled) { _, newValue in
|
|
if newValue {
|
|
Task {
|
|
let granted = await WalkHealthSync.shared.requestAuthorization()
|
|
if !granted {
|
|
healthKitSyncEnabled = false
|
|
showHealthPermissionAlert = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Aufnahme")
|
|
} footer: {
|
|
Text("Auto-Pause: pausiert die Aufnahme, wenn du 2 Minuten lang stehen bleibst.\nApple Health: schreibt jede gespeicherte Tour als Spaziergang-Workout mit Route in Health.")
|
|
}
|
|
|
|
Section("Mehr auf banyaro.app") {
|
|
pwaLink("Forum", systemImage: "bubble.left.and.bubble.right.fill", fragment: "forum")
|
|
pwaLink("Hunde-Profile bearbeiten", systemImage: "pawprint.fill", fragment: "dogs")
|
|
pwaLink("Gassi-Treffen", systemImage: "person.2.fill", fragment: "walks")
|
|
pwaLink("Profil & Einstellungen", systemImage: "gearshape.fill", fragment: "settings")
|
|
}
|
|
|
|
Section {
|
|
if let url = URL(string: "https://banyaro.app") {
|
|
Link(destination: url) {
|
|
Label("banyaro.app in Safari öffnen", systemImage: "safari.fill")
|
|
.foregroundStyle(.primary)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("banyaro.app als Web-App")
|
|
} footer: {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Du kannst banyaro.app zusätzlich als Web-App auf deinem Home-Bildschirm ablegen — praktisch für alle Features, die diese App nicht abbildet.")
|
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
|
Text("**1.**")
|
|
Text("Oben „in Safari öffnen“ tippen")
|
|
}
|
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
|
Text("**2.**")
|
|
HStack(spacing: 4) {
|
|
Text("In Safari unten das Teilen-Icon")
|
|
Image(systemName: "square.and.arrow.up")
|
|
Text("antippen")
|
|
}
|
|
}
|
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
|
Text("**3.**")
|
|
HStack(spacing: 4) {
|
|
Text("„Zum Home-Bildschirm“")
|
|
Image(systemName: "plus.square")
|
|
Text("auswählen")
|
|
}
|
|
}
|
|
Text("Eine native App kann eine Web-App leider nicht selbst installieren — Apple lässt das nur über Safari zu.")
|
|
.padding(.top, 4)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Button("Abmelden", role: .destructive) {
|
|
auth.logout()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Button(role: .destructive) {
|
|
showDeleteConfirm1 = true
|
|
} label: {
|
|
if isDeleting {
|
|
HStack { ProgressView(); Text("Wird gelöscht…") }
|
|
} else {
|
|
Label("Konto unwiderruflich löschen", systemImage: "trash")
|
|
}
|
|
}
|
|
.disabled(isDeleting)
|
|
if let deleteError {
|
|
Text(deleteError).font(.footnote).foregroundStyle(.red)
|
|
}
|
|
} header: {
|
|
Text("Konto löschen")
|
|
} footer: {
|
|
Text("Löscht dein banyaro-Konto, alle Hunde, Touren, Tagebuch-Einträge, Ausgaben und Fotos endgültig. Das gilt App-übergreifend (auch für banyaro.app im Browser). Die Aktion kann nicht rückgängig gemacht werden.")
|
|
}
|
|
|
|
Section("Über") {
|
|
Text("Ban Yaro Go ist die native iOS-Ergänzung zur banyaro.app PWA. Phase 1: deine Touren ansehen.")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.navigationTitle("Mehr")
|
|
.refreshable { await auth.loadProfile() }
|
|
.alert("Apple Health hat den Zugriff verweigert", isPresented: $showHealthPermissionAlert) {
|
|
Button("OK", role: .cancel) {}
|
|
} message: {
|
|
Text("Du kannst die Berechtigung in den iOS-Einstellungen unter Datenschutz & Sicherheit → Health → Ban Yaro Go nachträglich ändern.")
|
|
}
|
|
.alert("Konto wirklich löschen?", isPresented: $showDeleteConfirm1) {
|
|
Button("Abbrechen", role: .cancel) {}
|
|
Button("Weiter", role: .destructive) { showDeleteConfirm2 = true }
|
|
} message: {
|
|
Text("Alle Hunde, Touren, Tagebuch, Ausgaben und Fotos werden endgültig gelöscht — App und banyaro.app gleichermaßen. Diese Aktion kann nicht rückgängig gemacht werden.")
|
|
}
|
|
.alert("Letzte Bestätigung", isPresented: $showDeleteConfirm2) {
|
|
Button("Abbrechen", role: .cancel) {}
|
|
Button("Endgültig löschen", role: .destructive) {
|
|
Task { await deleteAccount() }
|
|
}
|
|
} message: {
|
|
Text("Bist du dir sicher? Dein Konto und alle Daten werden jetzt sofort und unwiderruflich entfernt.")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func deleteAccount() async {
|
|
isDeleting = true
|
|
deleteError = nil
|
|
defer { isDeleting = false }
|
|
do {
|
|
try await APIClient.shared.delete("/api/profile/account")
|
|
auth.logout()
|
|
} catch {
|
|
deleteError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private var displayName: String {
|
|
auth.profile?.name ?? auth.userName ?? "—"
|
|
}
|
|
|
|
private var premiumValue: Bool {
|
|
auth.profile?.isPremium ?? auth.isPremium
|
|
}
|
|
|
|
private var rolleLabel: String {
|
|
switch auth.profile?.rolle?.lowercased() {
|
|
case "admin": return "Admin"
|
|
case "moderator": return "Moderator"
|
|
case nil: return "—"
|
|
default: return "Mitglied"
|
|
}
|
|
}
|
|
|
|
private var founderLabel: String {
|
|
if let n = auth.profile?.founderNumber { return "#\(n)" }
|
|
return "Ja"
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var avatarView: some View {
|
|
if let path = auth.profile?.avatarUrl, !path.isEmpty,
|
|
let url = avatarURL(path) {
|
|
AsyncImage(url: url) { phase in
|
|
switch phase {
|
|
case .success(let img): img.resizable().scaledToFill()
|
|
default: avatarPlaceholder
|
|
}
|
|
}
|
|
} else {
|
|
avatarPlaceholder
|
|
}
|
|
}
|
|
|
|
private var avatarPlaceholder: some View {
|
|
ZStack {
|
|
Color.accentColor.opacity(0.2)
|
|
Image(systemName: "person.crop.circle.fill")
|
|
.font(.system(size: 36))
|
|
.foregroundStyle(Color.accentColor)
|
|
}
|
|
}
|
|
|
|
private func avatarURL(_ path: String) -> URL? {
|
|
if path.hasPrefix("http") { return URL(string: path) }
|
|
return URL(string: "https://banyaro.app\(path)")
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func pwaLink(_ title: String, systemImage: String, fragment: String) -> some View {
|
|
if let url = URL(string: "https://banyaro.app/#\(fragment)") {
|
|
Link(destination: url) {
|
|
HStack {
|
|
Label(title, systemImage: systemImage)
|
|
.foregroundStyle(.primary)
|
|
Spacer()
|
|
Image(systemName: "arrow.up.right.square")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|