Main coves client
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}