Ban Yaro Go — Phase 1 Foundation

SwiftUI/SwiftData iOS-Client, redet mit https://banyaro.app FastAPI-Backend.
Bundle-ID app.banyaro.ios, Xcode-26-Projekt mit synchronisierten Ordnern.

Drin:
- APIClient (URLSession + Bearer + convertFromSnakeCase Decoder)
- KeychainStore + AuthSession (@Observable) für persistenten Login
- LoginView, MainTabView, SettingsView (mit Logout)
- RoutesListView + RouteDetailView mit MapKit-Polyline aus preview_track
- DogsListView mit Foto-Avatar
- App-Icon (Pfote auf Banyaro-Amber)
This commit is contained in:
rene 2026-05-30 09:25:48 +02:00
commit 81681130e6
20 changed files with 1129 additions and 0 deletions

View file

@ -0,0 +1,50 @@
import Foundation
import Observation
@Observable
@MainActor
final class AuthSession {
var token: String?
var userName: String?
var isPremium: Bool = false
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
}
}
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
}
}

View file

@ -0,0 +1,42 @@
import Foundation
import Security
enum KeychainStore {
static let service = "app.banyaro.ios"
static func save(_ value: String, for key: String) {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
var item = query
item[kSecValueData as String] = data
SecItemAdd(item as CFDictionary, nil)
}
static func read(_ key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
static func delete(_ key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}