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.
76 lines
2.3 KiB
Swift
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)")
|
|
}
|
|
}
|
|
}
|