Files
honeyDueKMP/iosApp/iosApp/RootView.swift
Trey t efdb760438 Add interactive iOS widget with subscription-based views
- Add direct API completion from widget via quick-complete endpoint
- Share auth token and API URL with widget via App Group UserDefaults
- Add dirty flag mechanism to refresh tasks when app returns from background
- Widget checkbox colors indicate priority (red=urgent, orange=high, yellow=medium, green=low)
- Show simple "X tasks waiting" view for free tier users when limitations enabled
- Show interactive task completion widget for premium users or when limitations disabled
- Sync subscription status with widget extension for view selection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 12:02:16 -06:00

173 lines
6.0 KiB
Swift

import SwiftUI
import ComposeApp
/// Shared authentication state manager
class AuthenticationManager: ObservableObject {
static let shared = AuthenticationManager()
@Published var isAuthenticated: Bool = false
@Published var isVerified: Bool = false
@Published var isCheckingAuth: Bool = true
private init() {
checkAuthenticationStatus()
}
func checkAuthenticationStatus() {
isCheckingAuth = true
// Check if token exists via DataManager (single source of truth)
guard DataManager.shared.isAuthenticated() else {
isAuthenticated = false
isVerified = false
isCheckingAuth = false
return
}
isAuthenticated = true
// Fetch current user and initialize lookups immediately for all authenticated users
Task { @MainActor in
do {
// Initialize lookups right away for any authenticated user
// This fetches /static_data/ and /upgrade-triggers/ at app start
print("🚀 Initializing lookups at app start...")
_ = try await APILayer.shared.initializeLookups()
print("✅ Lookups initialized on app launch")
let result = try await APILayer.shared.getCurrentUser(forceRefresh: true)
if let success = result as? ApiResultSuccess<User> {
self.isVerified = success.data?.verified ?? false
// Register device for push notifications for authenticated users
PushNotificationManager.shared.registerDeviceAfterLogin()
// Verify subscription entitlements with backend for verified users
if self.isVerified {
await StoreKitManager.shared.verifyEntitlementsOnLaunch()
}
} else if result is ApiResultError {
// Token is invalid, clear all data via DataManager
DataManager.shared.clear()
self.isAuthenticated = false
self.isVerified = false
}
} catch {
print("❌ Failed to check auth status: \(error)")
// On error, assume token is invalid
DataManager.shared.clear()
self.isAuthenticated = false
self.isVerified = false
}
self.isCheckingAuth = false
}
}
func login(verified: Bool) {
isAuthenticated = true
isVerified = verified
// Register device for push notifications now that user is authenticated
PushNotificationManager.shared.registerDeviceAfterLogin()
}
func markVerified() {
isVerified = true
// Lookups are already initialized at app start or during login/register
// Just verify subscription entitlements after user becomes verified
Task {
await StoreKitManager.shared.verifyEntitlementsOnLaunch()
print("✅ Subscription entitlements verified after email verification")
}
}
func logout() {
// Call APILayer logout which clears DataManager
Task {
_ = try? await APILayer.shared.logout()
}
// Clear widget data (tasks and auth token)
WidgetDataManager.shared.clearCache()
WidgetDataManager.shared.clearAuthToken()
// Update authentication state
isAuthenticated = false
isVerified = false
// Note: We don't reset onboarding state on logout
// so returning users go to login screen, not onboarding
print("AuthenticationManager: Logged out - all state reset")
}
/// Reset onboarding state (for testing or re-onboarding)
func resetOnboarding() {
OnboardingState.shared.reset()
}
}
/// Root view that handles authentication flow: loading -> onboarding -> login -> verify email -> main app
struct RootView: View {
@EnvironmentObject private var themeManager: ThemeManager
@StateObject private var authManager = AuthenticationManager.shared
@StateObject private var onboardingState = OnboardingState.shared
@State private var refreshID = UUID()
var body: some View {
Group {
if authManager.isCheckingAuth {
// Show loading while checking auth status
loadingView
} else if !onboardingState.hasCompletedOnboarding {
// Show onboarding for first-time users (includes auth + verification steps)
// This takes precedence because we need to finish the onboarding flow
OnboardingCoordinator(onComplete: {
// Onboarding complete - mark verified and refresh the view
authManager.markVerified()
refreshID = UUID()
})
} else if !authManager.isAuthenticated {
// Show login screen for returning users
LoginView()
} else if !authManager.isVerified {
// Show email verification screen (for returning users who haven't verified)
VerifyEmailView(
onVerifySuccess: {
authManager.markVerified()
},
onLogout: {
authManager.logout()
}
)
} else {
// Show main app
MainTabView(refreshID: refreshID)
.onChange(of: themeManager.currentTheme) { _ in
refreshID = UUID()
}
}
}
}
private var loadingView: some View {
ZStack {
Color.appBackgroundPrimary
.ignoresSafeArea()
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.5)
.tint(Color.appPrimary)
Text("Loading...")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
}
}
}
}