- SwiftUI + async/await architecture - Barcode scanning with AVFoundation - Product display with score ring, NOVA badge, nutrition - Alternatives with sort/filter - Auth (login/register) - Favorites & history - Account management - Dark theme - Connected to food.payfrit.com API (Open Food Facts proxy) Co-Authored-By: Claude <noreply@anthropic.com>
119 lines
3.2 KiB
Swift
119 lines
3.2 KiB
Swift
import SwiftUI
|
|
|
|
@MainActor
|
|
class AppState: ObservableObject {
|
|
// MARK: - Authentication State
|
|
@Published var isAuthenticated = false
|
|
@Published var userId: Int?
|
|
@Published var userToken: String?
|
|
@Published var userProfile: UserProfile?
|
|
@Published var isPremium = false
|
|
|
|
// MARK: - Current Product State
|
|
@Published var currentProduct: Product?
|
|
@Published var currentAlternatives: [Alternative] = []
|
|
|
|
// MARK: - Navigation State
|
|
@Published var selectedTab: Tab = .scan
|
|
@Published var showLoginSheet = false
|
|
@Published var showRegisterSheet = false
|
|
|
|
// MARK: - Loading State
|
|
@Published var isLoading = false
|
|
@Published var errorMessage: String?
|
|
|
|
enum Tab: Int, CaseIterable {
|
|
case scan = 0
|
|
case favorites = 1
|
|
case history = 2
|
|
case account = 3
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .scan: return "Scan"
|
|
case .favorites: return "Favorites"
|
|
case .history: return "History"
|
|
case .account: return "Account"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .scan: return "barcode.viewfinder"
|
|
case .favorites: return "heart.fill"
|
|
case .history: return "clock.fill"
|
|
case .account: return "person.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
init() {
|
|
Task {
|
|
await loadSavedAuth()
|
|
}
|
|
}
|
|
|
|
// MARK: - Authentication
|
|
func loadSavedAuth() async {
|
|
guard let credentials = await AuthStorage.shared.loadCredentials() else { return }
|
|
|
|
self.userId = credentials.userId
|
|
self.userToken = credentials.token
|
|
|
|
await APIService.shared.setToken(credentials.token)
|
|
|
|
// Try to fetch profile
|
|
do {
|
|
let profile = try await APIService.shared.getProfile()
|
|
self.userProfile = profile
|
|
self.isPremium = profile.isPremium
|
|
self.isAuthenticated = true
|
|
} catch {
|
|
// Token may be expired
|
|
await clearAuth()
|
|
}
|
|
}
|
|
|
|
func setAuth(userId: Int, token: String, profile: UserProfile) async {
|
|
self.userId = userId
|
|
self.userToken = token
|
|
self.userProfile = profile
|
|
self.isPremium = profile.isPremium
|
|
self.isAuthenticated = true
|
|
|
|
await APIService.shared.setToken(token)
|
|
await AuthStorage.shared.saveCredentials(AuthCredentials(userId: userId, token: token))
|
|
}
|
|
|
|
func clearAuth() async {
|
|
self.userId = nil
|
|
self.userToken = nil
|
|
self.userProfile = nil
|
|
self.isPremium = false
|
|
self.isAuthenticated = false
|
|
|
|
await APIService.shared.clearToken()
|
|
await AuthStorage.shared.clearAll()
|
|
}
|
|
|
|
// MARK: - Product Actions
|
|
func setCurrentProduct(_ product: Product) {
|
|
self.currentProduct = product
|
|
self.currentAlternatives = []
|
|
}
|
|
|
|
func clearCurrentProduct() {
|
|
self.currentProduct = nil
|
|
self.currentAlternatives = []
|
|
}
|
|
|
|
// MARK: - Error Handling
|
|
func showError(_ message: String) {
|
|
self.errorMessage = message
|
|
Task {
|
|
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
|
self.errorMessage = nil
|
|
}
|
|
}
|
|
}
|