1import 'package:flutter/foundation.dart'; 2 3import '../models/coves_session.dart'; 4import '../services/coves_auth_service.dart'; 5 6/// Authentication Provider 7/// 8/// Manages authentication state using the Coves backend OAuth flow. 9/// Uses ChangeNotifier for reactive state updates across the app. 10/// 11/// Key features: 12/// - Uses CovesAuthService for backend-managed OAuth 13/// - Tokens are sealed (AES-256-GCM encrypted) and opaque to the client 14/// - Backend handles DPoP, PKCE, and token refresh internally 15/// - Session stored securely (iOS Keychain / Android EncryptedSharedPreferences) 16class AuthProvider with ChangeNotifier { 17 /// Constructor with optional auth service for dependency injection 18 AuthProvider({CovesAuthService? authService}) 19 : _authService = authService ?? CovesAuthService(); 20 final CovesAuthService _authService; 21 22 // Session state 23 CovesSession? _session; 24 bool _isAuthenticated = false; 25 bool _isLoading = true; 26 String? _error; 27 28 // Getters 29 CovesSession? get session => _session; 30 bool get isAuthenticated => _isAuthenticated; 31 bool get isLoading => _isLoading; 32 String? get error => _error; 33 String? get did => _session?.did; 34 String? get handle => _session?.handle; 35 36 /// Get the current access token (sealed token) 37 /// 38 /// Returns the sealed token for API authentication. 39 /// The token is opaque to the client - backend handles everything. 40 /// 41 /// If token refresh fails, attempts to refresh automatically. 42 /// If refresh fails, signs out the user. 43 Future<String?> getAccessToken() async { 44 if (_session == null) { 45 return null; 46 } 47 48 // Return the sealed token directly 49 // Token refresh is handled by the backend when the token is used 50 return _session!.token; 51 } 52 53 /// Initialize the provider and restore any existing session 54 /// 55 /// This is called on app startup to: 56 /// 1. Initialize the auth service 57 /// 2. Restore session from secure storage if available 58 Future<void> initialize() async { 59 try { 60 _isLoading = true; 61 _error = null; 62 notifyListeners(); 63 64 // Initialize auth service 65 await _authService.initialize(); 66 67 // Try to restore a previous session from secure storage 68 final restoredSession = await _authService.restoreSession(); 69 70 if (restoredSession != null) { 71 _session = restoredSession; 72 _isAuthenticated = true; 73 74 if (kDebugMode) { 75 print('Restored session'); 76 print(' DID: ${restoredSession.did}'); 77 print(' Handle: ${restoredSession.handle}'); 78 } 79 } else { 80 if (kDebugMode) { 81 print('No stored session found - user not logged in'); 82 } 83 } 84 } catch (e) { 85 // Catch all errors to prevent app crashes during initialization 86 _error = e.toString(); 87 if (kDebugMode) { 88 print('Failed to initialize auth: $e'); 89 } 90 } finally { 91 _isLoading = false; 92 notifyListeners(); 93 } 94 } 95 96 /// Sign in with an atProto handle 97 /// 98 /// Opens the system browser to the backend's OAuth endpoint. 99 /// The backend handles: 100 /// - Handle -> DID resolution 101 /// - PDS discovery 102 /// - OAuth authorization with PKCE/DPoP 103 /// - Token sealing 104 /// 105 /// Works with ANY handle on ANY PDS: 106 /// - alice.bsky.social -> Bluesky PDS 107 /// - bob.custom-pds.com -> Custom PDS 108 /// - did:plc:abc123 -> Direct DID 109 Future<void> signIn(String handle) async { 110 try { 111 _isLoading = true; 112 _error = null; 113 notifyListeners(); 114 115 // Validate handle format 116 final trimmedHandle = handle.trim(); 117 if (trimmedHandle.isEmpty) { 118 throw Exception('Please enter a valid handle'); 119 } 120 121 // Perform OAuth sign in via backend 122 final session = await _authService.signIn(trimmedHandle); 123 124 // Update state 125 _session = session; 126 _isAuthenticated = true; 127 128 if (kDebugMode) { 129 print('Successfully signed in'); 130 print(' Handle: ${session.handle ?? trimmedHandle}'); 131 print(' DID: ${session.did}'); 132 } 133 } catch (e) { 134 _error = e.toString(); 135 _isAuthenticated = false; 136 _session = null; 137 138 if (kDebugMode) { 139 print('Sign in failed: $e'); 140 } 141 142 rethrow; 143 } finally { 144 _isLoading = false; 145 notifyListeners(); 146 } 147 } 148 149 /// Sign out and clear session 150 /// 151 /// This: 152 /// 1. Calls the backend's logout endpoint (revokes session server-side) 153 /// 2. Clears session from secure storage 154 /// 3. Resets the provider state 155 Future<void> signOut() async { 156 try { 157 _isLoading = true; 158 notifyListeners(); 159 160 // Call auth service signOut (handles server + local cleanup) 161 await _authService.signOut(); 162 163 // Clear state 164 _session = null; 165 _isAuthenticated = false; 166 _error = null; 167 168 if (kDebugMode) { 169 print('Successfully signed out'); 170 } 171 } on Exception catch (e) { 172 _error = e.toString(); 173 if (kDebugMode) { 174 print('Sign out failed: $e'); 175 } 176 177 // Even if server revocation fails, clear local state 178 _session = null; 179 _isAuthenticated = false; 180 } finally { 181 _isLoading = false; 182 notifyListeners(); 183 } 184 } 185 186 /// Refresh the current session token 187 /// 188 /// Calls the backend's /oauth/refresh endpoint. 189 /// The backend handles the actual PDS token refresh internally. 190 /// 191 /// Returns true if refresh succeeded, false otherwise. 192 Future<bool> refreshToken() async { 193 if (_session == null) { 194 return false; 195 } 196 197 try { 198 final refreshedSession = await _authService.refreshToken(); 199 _session = refreshedSession; 200 notifyListeners(); 201 202 if (kDebugMode) { 203 print('Token refreshed successfully'); 204 } 205 206 return true; 207 } on Exception catch (e) { 208 if (kDebugMode) { 209 print('Token refresh failed: $e'); 210 } 211 212 // If refresh fails, sign out the user 213 await signOut(); 214 return false; 215 } 216 } 217 218 /// Clear error message 219 void clearError() { 220 _error = null; 221 notifyListeners(); 222 } 223}