- 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>
173 lines
6.0 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|