/// Example usage of FlutterOAuthClient for atProto OAuth authentication. /// /// This demonstrates the complete OAuth flow for a Flutter application: /// 1. Initialize the client /// 2. Sign in with a handle /// 3. Use the authenticated session /// 4. Restore session on app restart /// 5. Sign out (revoke session) import 'package:atproto_oauth_flutter/atproto_oauth_flutter.dart'; void main() async { // ======================================================================== // 1. Initialize the OAuth client // ======================================================================== final client = FlutterOAuthClient( clientMetadata: ClientMetadata( // For development: use loopback client (no client metadata URL needed) clientId: 'http://localhost', // For production: use discoverable client metadata // clientId: 'https://example.com/client-metadata.json', // Redirect URIs for your app // - Custom URL scheme: myapp://oauth/callback // - Universal links: https://example.com/oauth/callback redirectUris: ['myapp://oauth/callback'], // Scope: what permissions to request // - 'atproto': Full atproto access // - 'transition:generic': Additional permissions for legacy systems scope: 'atproto transition:generic', // Client metadata clientName: 'My Awesome App', clientUri: 'https://example.com', // Token binding dpopBoundAccessTokens: true, // Enable DPoP for security ), // Response mode (query or fragment) responseMode: OAuthResponseMode.query, // Allow HTTP only for development (never in production!) allowHttp: false, ); // ======================================================================== // 2. Sign in with a handle // ======================================================================== try { print('Starting sign-in flow for alice.bsky.social...'); // This will: // 1. Resolve the handle to find the authorization server // 2. Generate PKCE code challenge/verifier // 3. Generate DPoP key // 4. Open browser for user authentication // 5. Handle OAuth callback // 6. Exchange authorization code for tokens // 7. Store session securely final session = await client.signIn('alice.bsky.social'); print('✓ Signed in successfully!'); print(' DID: ${session.sub}'); print(' Session info: ${session.info}'); // ======================================================================== // 3. Use the authenticated session // ======================================================================== // The session has a PDS client you can use for authenticated requests // (This requires integrating with an atproto API client library) // // Example: // final agent = session.pdsClient; // final profile = await agent.getProfile(); print('Session is ready for API calls'); } on OAuthCallbackError catch (e) { // Handle OAuth errors (user cancelled, invalid state, etc.) print('OAuth callback error: ${e.error}'); print('Description: ${e.errorDescription}'); return; } catch (e) { print('Sign-in error: $e'); return; } // ======================================================================== // 4. Restore session on app restart // ======================================================================== // Later, when the app restarts, restore the session: try { final did = 'did:plc:abc123'; // Get from storage or previous session print('Restoring session for $did...'); // This will: // 1. Load session from secure storage // 2. Check if tokens are expired // 3. Automatically refresh if needed // 4. Return authenticated session final session = await client.restore(did); print('✓ Session restored!'); print(' Access token expires: ${session.info['expiresAt']}'); } catch (e) { print('Failed to restore session: $e'); // Session may have been revoked or expired // Prompt user to sign in again } // ======================================================================== // 5. Sign out (revoke session) // ======================================================================== try { final did = 'did:plc:abc123'; print('Signing out $did...'); // This will: // 1. Call token revocation endpoint (best effort) // 2. Delete session from secure storage // 3. Emit 'deleted' event await client.revoke(did); print('✓ Signed out successfully'); } catch (e) { print('Sign out error: $e'); // Session is still deleted locally even if revocation fails } // ======================================================================== // Advanced: Listen to session events // ======================================================================== // Listen for session updates (token refresh, etc.) client.onUpdated.listen((event) { print('Session updated: ${event.sub}'); print(' New access token received'); }); // Listen for session deletions (revoked, expired, etc.) client.onDeleted.listen((event) { print('Session deleted: ${event.sub}'); print(' Cause: ${event.cause}'); // Handle session deletion (navigate to sign-in screen, etc.) }); // ======================================================================== // Advanced: Custom configuration // ======================================================================== // You can customize storage, caching, and crypto: final customClient = FlutterOAuthClient( clientMetadata: ClientMetadata( clientId: 'https://example.com/client-metadata.json', redirectUris: ['myapp://oauth/callback'], ), // Custom secure storage instance secureStorage: const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), ), // Custom PLC directory URL (for private deployments) plcDirectoryUrl: 'https://plc.example.com', // Custom handle resolver URL handleResolverUrl: 'https://bsky.social', ); print('Custom client initialized'); // ======================================================================== // Platform configuration (iOS) // ======================================================================== // iOS: Add URL scheme to Info.plist // CFBundleURLTypes // // // CFBundleURLSchemes // // myapp // // // // ======================================================================== // Platform configuration (Android) // ======================================================================== // Android: Add intent filter to AndroidManifest.xml // // // // // // // ======================================================================== // Security best practices // ======================================================================== // ✓ Tokens stored in secure storage (Keychain/EncryptedSharedPreferences) // ✓ DPoP binds tokens to cryptographic keys // ✓ PKCE prevents authorization code interception // ✓ State parameter prevents CSRF attacks // ✓ Automatic token refresh with concurrency control // ✓ Session cleanup on errors print('Example complete!'); }