1/// Example usage of FlutterOAuthClient for atProto OAuth authentication. 2/// 3/// This demonstrates the complete OAuth flow for a Flutter application: 4/// 1. Initialize the client 5/// 2. Sign in with a handle 6/// 3. Use the authenticated session 7/// 4. Restore session on app restart 8/// 5. Sign out (revoke session) 9 10import 'package:atproto_oauth_flutter/atproto_oauth_flutter.dart'; 11 12void main() async { 13 // ======================================================================== 14 // 1. Initialize the OAuth client 15 // ======================================================================== 16 17 final client = FlutterOAuthClient( 18 clientMetadata: ClientMetadata( 19 // For development: use loopback client (no client metadata URL needed) 20 clientId: 'http://localhost', 21 22 // For production: use discoverable client metadata 23 // clientId: 'https://example.com/client-metadata.json', 24 25 // Redirect URIs for your app 26 // - Custom URL scheme: myapp://oauth/callback 27 // - Universal links: https://example.com/oauth/callback 28 redirectUris: ['myapp://oauth/callback'], 29 30 // Scope: what permissions to request 31 // - 'atproto': Full atproto access 32 // - 'transition:generic': Additional permissions for legacy systems 33 scope: 'atproto transition:generic', 34 35 // Client metadata 36 clientName: 'My Awesome App', 37 clientUri: 'https://example.com', 38 39 // Token binding 40 dpopBoundAccessTokens: true, // Enable DPoP for security 41 ), 42 43 // Response mode (query or fragment) 44 responseMode: OAuthResponseMode.query, 45 46 // Allow HTTP only for development (never in production!) 47 allowHttp: false, 48 ); 49 50 // ======================================================================== 51 // 2. Sign in with a handle 52 // ======================================================================== 53 54 try { 55 print('Starting sign-in flow for alice.bsky.social...'); 56 57 // This will: 58 // 1. Resolve the handle to find the authorization server 59 // 2. Generate PKCE code challenge/verifier 60 // 3. Generate DPoP key 61 // 4. Open browser for user authentication 62 // 5. Handle OAuth callback 63 // 6. Exchange authorization code for tokens 64 // 7. Store session securely 65 final session = await client.signIn('alice.bsky.social'); 66 67 print('✓ Signed in successfully!'); 68 print(' DID: ${session.sub}'); 69 print(' Session info: ${session.info}'); 70 71 // ======================================================================== 72 // 3. Use the authenticated session 73 // ======================================================================== 74 75 // The session has a PDS client you can use for authenticated requests 76 // (This requires integrating with an atproto API client library) 77 // 78 // Example: 79 // final agent = session.pdsClient; 80 // final profile = await agent.getProfile(); 81 82 print('Session is ready for API calls'); 83 } on OAuthCallbackError catch (e) { 84 // Handle OAuth errors (user cancelled, invalid state, etc.) 85 print('OAuth callback error: ${e.error}'); 86 print('Description: ${e.errorDescription}'); 87 return; 88 } catch (e) { 89 print('Sign-in error: $e'); 90 return; 91 } 92 93 // ======================================================================== 94 // 4. Restore session on app restart 95 // ======================================================================== 96 97 // Later, when the app restarts, restore the session: 98 try { 99 final did = 'did:plc:abc123'; // Get from storage or previous session 100 101 print('Restoring session for $did...'); 102 103 // This will: 104 // 1. Load session from secure storage 105 // 2. Check if tokens are expired 106 // 3. Automatically refresh if needed 107 // 4. Return authenticated session 108 final session = await client.restore(did); 109 110 print('✓ Session restored!'); 111 print(' Access token expires: ${session.info['expiresAt']}'); 112 } catch (e) { 113 print('Failed to restore session: $e'); 114 // Session may have been revoked or expired 115 // Prompt user to sign in again 116 } 117 118 // ======================================================================== 119 // 5. Sign out (revoke session) 120 // ======================================================================== 121 122 try { 123 final did = 'did:plc:abc123'; 124 125 print('Signing out $did...'); 126 127 // This will: 128 // 1. Call token revocation endpoint (best effort) 129 // 2. Delete session from secure storage 130 // 3. Emit 'deleted' event 131 await client.revoke(did); 132 133 print('✓ Signed out successfully'); 134 } catch (e) { 135 print('Sign out error: $e'); 136 // Session is still deleted locally even if revocation fails 137 } 138 139 // ======================================================================== 140 // Advanced: Listen to session events 141 // ======================================================================== 142 143 // Listen for session updates (token refresh, etc.) 144 client.onUpdated.listen((event) { 145 print('Session updated: ${event.sub}'); 146 print(' New access token received'); 147 }); 148 149 // Listen for session deletions (revoked, expired, etc.) 150 client.onDeleted.listen((event) { 151 print('Session deleted: ${event.sub}'); 152 print(' Cause: ${event.cause}'); 153 // Handle session deletion (navigate to sign-in screen, etc.) 154 }); 155 156 // ======================================================================== 157 // Advanced: Custom configuration 158 // ======================================================================== 159 160 // You can customize storage, caching, and crypto: 161 final customClient = FlutterOAuthClient( 162 clientMetadata: ClientMetadata( 163 clientId: 'https://example.com/client-metadata.json', 164 redirectUris: ['myapp://oauth/callback'], 165 ), 166 167 // Custom secure storage instance 168 secureStorage: const FlutterSecureStorage( 169 aOptions: AndroidOptions(encryptedSharedPreferences: true), 170 ), 171 172 // Custom PLC directory URL (for private deployments) 173 plcDirectoryUrl: 'https://plc.example.com', 174 175 // Custom handle resolver URL 176 handleResolverUrl: 'https://bsky.social', 177 ); 178 179 print('Custom client initialized'); 180 181 // ======================================================================== 182 // Platform configuration (iOS) 183 // ======================================================================== 184 185 // iOS: Add URL scheme to Info.plist 186 // <key>CFBundleURLTypes</key> 187 // <array> 188 // <dict> 189 // <key>CFBundleURLSchemes</key> 190 // <array> 191 // <string>myapp</string> 192 // </array> 193 // </dict> 194 // </array> 195 196 // ======================================================================== 197 // Platform configuration (Android) 198 // ======================================================================== 199 200 // Android: Add intent filter to AndroidManifest.xml 201 // <intent-filter> 202 // <action android:name="android.intent.action.VIEW" /> 203 // <category android:name="android.intent.category.DEFAULT" /> 204 // <category android:name="android.intent.category.BROWSABLE" /> 205 // <data android:scheme="myapp" /> 206 // </intent-filter> 207 208 // ======================================================================== 209 // Security best practices 210 // ======================================================================== 211 212 // ✓ Tokens stored in secure storage (Keychain/EncryptedSharedPreferences) 213 // ✓ DPoP binds tokens to cryptographic keys 214 // ✓ PKCE prevents authorization code interception 215 // ✓ State parameter prevents CSRF attacks 216 // ✓ Automatic token refresh with concurrency control 217 // ✓ Session cleanup on errors 218 219 print('Example complete!'); 220}