banyaro-ios/BanYaroGo/Auth/AuthSession.swift
rene c01e3d6be7 Phase 3.6: B+C+D komplett + HealthKit Sync
D.10 401-Handling: APIError.unauthorized, NotificationCenter-Bridge,
  AuthSession.logout() bei 401 → User landet wieder im Login

D.12 PWA-Deep-Links: Settings-Section mit Forum/Hunde/Walks/Settings
  öffnet Safari per https://banyaro.app/#fragment

B.4 Auto-Pause: 2-min-Inaktivität → isAutoPaused, automatischer Resume bei
  nächstem GPS-Update. Settings-Toggle, im UI eigenes Badge "Auto-Pause"
  (grau vs. Pause orange).

C.7 Edit/Delete: RouteUpdateBody + APIClient.patch + APIClient.delete,
  EditRouteSheet (Name/Beschreibung/Public), Menu in Toolbar (nur eigene
  Touren), Alert für Delete.

C.9 Statistik-Tab: neuer Tab "Statistik" zwischen Hunde und Mehr. Filtert
  /api/routes auf meine Touren, rechnet Woche/Monat/Allzeit (Distanz, Dauer,
  Touren), Längste Tour, aktuelle Streak (Tage in Folge).

B.5 Walk-Review: Map-Header an die Spitze des FinishWalkSheet-Forms.

B.6 Geo-Fotos: CapturedPhoto (Data + GPSPoint?), PhotoLocation @Model in
  SwiftData. Kamera während Walk taggt mit tracker.points.last. Nach Upload:
  foto_url aus Response → PhotoLocation persistiert. MiniRouteMap rendert
  Annotations mit Tap-Callback, PhotoViewerSheet zeigt Foto fullscreen.

C.8 Share PNG+GPX: RouteShareImage (MKMapSnapshotter + Polyline overlay +
  SwiftUI ShareCard via ImageRenderer), GPXExporter (Tempfile mit XML),
  ShareSheet (UIActivityViewController-Wrapper), Menu in Route-Toolbar.

D.11 Icon-Varianten: AppIcon-Dark (0.45 Brightness), AppIcon-Tinted
  (Grayscale + Kontrastverstärkung), Contents.json mit appearance entries.

A.2 HealthKit: BanYaroGo.entitlements (com.apple.developer.healthkit),
  NSHealthShare/UpdateUsageDescription. WalkHealthSync.shared mit
  HKWorkoutBuilder (.walking) + HKWorkoutRouteBuilder, Timestamps gleichmäßig
  über Walk-Dauer verteilt. Settings-Toggle mit Permission-Request.
2026-05-30 11:19:53 +02:00

76 lines
2.3 KiB
Swift

import Foundation
import Observation
@Observable
@MainActor
final class AuthSession {
var token: String?
var userName: String?
var isPremium: Bool = false
var profile: UserProfile?
var isLoggingIn: Bool = false
var errorMessage: String?
private let tokenKey = "by_token"
init() {
if let savedToken = KeychainStore.read(tokenKey) {
token = savedToken
APIClient.shared.token = savedToken
}
NotificationCenter.default.addObserver(
forName: .apiUnauthorized,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in self?.logout() }
}
}
var isLoggedIn: Bool { token != nil }
func login(email: String, password: String) async {
isLoggingIn = true
errorMessage = nil
defer { isLoggingIn = false }
do {
let response: LoginResponse = try await APIClient.shared.post(
"/api/auth/login",
body: LoginRequest(email: email, password: password)
)
KeychainStore.save(response.token, for: tokenKey)
APIClient.shared.token = response.token
self.token = response.token
self.userName = response.name
self.isPremium = response.isPremium
} catch {
self.errorMessage = error.localizedDescription
}
}
func logout() {
KeychainStore.delete(tokenKey)
APIClient.shared.token = nil
token = nil
userName = nil
isPremium = false
profile = nil
}
/// Fetches the full user profile from /api/auth/me. Called after login and
/// when MainTabView appears, so admin/founder/role info shows up.
func loadProfile() async {
guard token != nil else { return }
do {
let me: UserProfile = try await APIClient.shared.get("/api/auth/me")
self.profile = me
if let premium = me.isPremium {
self.isPremium = premium
}
} catch {
// Profil-Refresh ist non-critical; Settings zeigt sonst nur die Basics.
// Loggen ist trotzdem nützlich, damit Decode-Fehler nicht stillschweigend untergehen.
print("loadProfile failed: \(error)")
}
}
}