Main coves client
1# OAuth Implementation Guide for Coves Flutter
2
3## Overview
4
5This document outlines the OAuth implementation for Coves, a forum-like atProto social media platform. We're using the `atproto_oauth` package (v0.1.0) to authenticate users against their Personal Data Servers (PDS) in the decentralized atProto network.
6
7## ⚠️ Important: Decentralized Authentication
8
9**atProto is a decentralized protocol** - users can be on ANY Personal Data Server (PDS), not just bsky.social!
10
11### How Handle Resolution Works
12
13The OAuth flow must support users from any PDS:
14
151. **Handle Resolver** (`service: 'bsky.social'` parameter)
16 - This is a service that can resolve ANY atProto handle to a DID
17 - Example: `alice.pds.example.com` → `did:plc:abc123`
18 - Bluesky provides a public resolver that works for all atProto handles
19 - **This is NOT the authorization server!**
20
212. **PDS Discovery** (automatic via DID document)
22 - User's DID document contains their PDS URL
23 - Fetch `did:plc:abc123` → Find PDS endpoint in service array
24 - Example: `https://alice-pds.example.com`
25
263. **OAuth Authorization Server Discovery** (automatic)
27 - Each PDS has its own OAuth server
28 - Discovered from: `https://alice-pds.example.com/.well-known/oauth-authorization-server`
29 - **Users are redirected to THEIR PDS's auth server, not always bsky.social**
30
31### Testing Decentralization
32
33When testing sign-in, check the debug logs:
34
35```dart
36🔍 OAuth Authorization URL: https://example-pds.com/oauth/authorize?...
37🔍 Authorization server host: example-pds.com
38```
39
40✅ **Correct**: `authUrl.host` matches the user's PDS
41❌ **Wrong**: `authUrl.host` is always `bsky.app` regardless of handle
42
43If users on different PDSes can sign in (not just bsky.social users), then decentralization is working correctly!
44
45---
46
47## ✅ What's Already Been Completed
48
49### 1. Project Setup & Dependencies
50
51**File:** [`pubspec.yaml`](./pubspec.yaml)
52
53All required OAuth packages are installed and configured:
54- ✅ `atproto_oauth: ^0.1.0` - Official atProto OAuth client
55- ✅ `flutter_web_auth_2: ^4.1.0` - Browser-based OAuth flow
56- ✅ `flutter_secure_storage: ^9.2.2` - Encrypted token storage
57- ✅ `go_router: ^16.3.0` - Navigation with deep linking support
58- ✅ `provider: ^6.1.5+1` - State management
59
60### 2. OAuth Configuration
61
62**File:** [`lib/config/oauth_config.dart`](./lib/config/oauth_config.dart)
63
64Complete OAuth configuration matching your Cloudflare Worker setup:
65
66```dart
67class OAuthConfig {
68 // OAuth Server (Cloudflare Worker)
69 static const String oauthServerUrl =
70 'https://lingering-darkness-50a6.brettmay0212.workers.dev';
71
72 // Custom URL scheme for deep linking
73 static const String customScheme =
74 'dev.workers.brettmay0212.lingering-darkness-50a6';
75
76 // Client metadata URL (hosted on your Cloudflare Worker)
77 static const String clientId = '$oauthServerUrl/client-metadata.json';
78
79 // OAuth callback URL
80 static const String redirectUri = '$oauthServerUrl/oauth/callback';
81
82 // atProto scopes
83 static const String scope = 'atproto transition:generic';
84
85 // Handle resolver (uses Bluesky's resolver)
86 static const String handleResolver = 'https://bsky.social';
87}
88```
89
90**Key Points:**
91- ✅ All URLs point to your Cloudflare Worker
92- ✅ Client metadata is hosted at `/client-metadata.json`
93- ✅ Custom scheme matches your app configuration
94- ✅ Scopes allow full atProto access
95
96### 3. OAuth Service Foundation
97
98**File:** [`lib/services/oauth_service.dart`](./lib/services/oauth_service.dart)
99
100OAuth service skeleton with proper architecture:
101
102```dart
103class OAuthService {
104 OAuthClient? _client;
105 final _storage = const FlutterSecureStorage();
106
107 // Storage keys - properly named for atProto decentralization
108 static const _keyAccessToken = 'atproto_oauth_access_token';
109 static const _keyRefreshToken = 'atproto_oauth_refresh_token';
110 static const _keyDid = 'atproto_did';
111 static const _keyHandle = 'atproto_handle';
112
113 Future<void> initialize() async {
114 // Fetches client metadata from Cloudflare Worker
115 final metadata = await getClientMetadata(OAuthConfig.clientId);
116 _client = OAuthClient(metadata, service: 'bsky.social');
117 }
118
119 // Methods ready for implementation:
120 Future<OAuthSession> signIn(String handle) async { /* ... */ }
121 Future<OAuthSession?> restoreSession() async { /* ... */ }
122 Future<void> signOut() async { /* ... */ }
123}
124```
125
126**What's Ready:**
127- ✅ Singleton pattern for service
128- ✅ Client initialization with metadata fetching
129- ✅ Secure storage using `flutter_secure_storage`
130- ✅ Storage keys properly named for atProto (not app-specific)
131- ✅ Session management methods scaffolded
132
133**Important Design Decision:**
134Storage keys use `atproto_*` prefix instead of `coves_*` to reflect that credentials belong to the user's PDS, not to Coves. This follows the decentralized architecture principle where the user owns their identity.
135
136### 4. Authentication State Management
137
138**File:** [`lib/providers/auth_provider.dart`](./lib/providers/auth_provider.dart)
139
140Complete auth provider using `ChangeNotifier`:
141
142```dart
143class AuthProvider with ChangeNotifier {
144 final OAuthService _oauthService = OAuthService();
145
146 // State
147 OAuthSession? _session;
148 bool _isAuthenticated = false;
149 bool _isLoading = true;
150 String? _error;
151 String? _did;
152 String? _handle;
153
154 // Methods
155 Future<void> initialize() async { /* Restores session */ }
156 Future<void> signIn(String handle) async { /* OAuth flow */ }
157 Future<void> signOut() async { /* Revokes & clears */ }
158 void clearError() { /* Error handling */ }
159}
160```
161
162**What's Ready:**
163- ✅ Reactive state management with `ChangeNotifier`
164- ✅ Loading states for UI feedback
165- ✅ Error handling
166- ✅ Session persistence
167- ✅ Integrated with `OAuthService`
168
169### 5. Android Deep Link Configuration
170
171**File:** [`android/app/src/main/AndroidManifest.xml`](./android/app/src/main/AndroidManifest.xml)
172
173Deep links configured for OAuth callbacks:
174
175```xml
176<!-- HTTPS deep link for OAuth callback -->
177<intent-filter android:autoVerify="true">
178 <action android:name="android.intent.action.VIEW"/>
179 <category android:name="android.intent.category.DEFAULT"/>
180 <category android:name="android.intent.category.BROWSABLE"/>
181
182 <data
183 android:scheme="https"
184 android:host="lingering-darkness-50a6.brettmay0212.workers.dev"
185 android:pathPrefix="/oauth/callback"/>
186</intent-filter>
187
188<!-- Custom scheme fallback -->
189<intent-filter>
190 <action android:name="android.intent.action.VIEW"/>
191 <category android:name="android.intent.category.DEFAULT"/>
192 <category android:name="android.intent.category.BROWSABLE"/>
193
194 <data android:scheme="dev.workers.brettmay0212.lingering-darkness-50a6"/>
195</intent-filter>
196```
197
198**What's Ready:**
199- ✅ HTTPS deep links (preferred on Android)
200- ✅ Custom scheme fallback
201- ✅ Auto-verify for App Links
202- ✅ Matches OAuth redirect URIs
203
204### 6. Login UI
205
206**File:** [`lib/screens/auth/login_screen.dart`](./lib/screens/auth/login_screen.dart)
207
208Professional login screen with:
209- ✅ Handle input with validation
210- ✅ Loading states
211- ✅ Error handling with SnackBar
212- ✅ Help dialog explaining handles
213- ✅ Integration with AuthProvider
214- ✅ Navigation to feed on success
215
216### 7. App-Level Integration
217
218**File:** [`lib/main.dart`](./lib/main.dart)
219
220Auth provider wrapped around entire app:
221
222```dart
223void main() async {
224 WidgetsFlutterBinding.ensureInitialized();
225
226 final authProvider = AuthProvider();
227 await authProvider.initialize();
228
229 runApp(
230 ChangeNotifierProvider.value(
231 value: authProvider,
232 child: const CovesApp(),
233 ),
234 );
235}
236```
237
238**What's Ready:**
239- ✅ Provider initialization before app starts
240- ✅ Session restoration on app launch
241- ✅ Global state access via `Provider.of<AuthProvider>(context)`
242
243---
244
245## 🚧 What Needs to Be Implemented
246
247### Phase 1: Complete OAuth Flow (High Priority)
248
249#### 1.1 Implement `signIn()` Method
250
251**File to Update:** [`lib/services/oauth_service.dart`](./lib/services/oauth_service.dart)
252
253**Current Status:** Method stub exists but returns `UnimplementedError`
254
255**What to Implement:**
256
257```dart
258Future<OAuthSession> signIn(String handle) async {
259 try {
260 if (_client == null) {
261 throw Exception('OAuth client not initialized');
262 }
263
264 // Step 1: Use atproto_oauth to resolve handle and build auth URL
265 // The package handles:
266 // - DID resolution from handle
267 // - Finding the user's authorization server
268 // - Generating PKCE challenge/verifier
269 // - Building PAR (Pushed Authorization Request)
270 // - Generating DPoP keys
271
272 final authRequest = await _client!.authorize(
273 identifier: handle,
274 // The package will use the client metadata we fetched
275 );
276
277 // Step 2: Open browser for user authorization
278 // This opens the user's PDS authorization page
279 final callbackUrl = await FlutterWebAuth2.authenticate(
280 url: authRequest.authorizationUrl.toString(),
281 callbackUrlScheme: OAuthConfig.customScheme,
282 );
283
284 // Step 3: Extract authorization code from callback
285 final uri = Uri.parse(callbackUrl);
286 final code = uri.queryParameters['code'];
287 final state = uri.queryParameters['state'];
288
289 if (code == null) {
290 throw Exception('No authorization code received');
291 }
292
293 // Step 4: Exchange code for tokens with DPoP
294 // The package handles:
295 // - Token exchange request
296 // - DPoP proof generation
297 // - Token validation
298 final session = await _client!.callback(
299 uri: uri,
300 // Package manages PKCE and state internally
301 );
302
303 // Step 5: Extract and store session data
304 final tokens = OAuthSession(
305 accessToken: session.accessToken,
306 refreshToken: session.refreshToken,
307 did: session.sub, // User's DID
308 handle: handle,
309 );
310
311 await _storeSession(tokens);
312
313 return tokens;
314 } catch (e) {
315 print('Sign in failed: $e');
316 rethrow;
317 }
318}
319```
320
321**Key Implementation Notes:**
322- Use `atproto_oauth`'s built-in methods for authorization flow
323- The package handles complex atProto specifics (DPoP, PKCE, PAR)
324- Store DID (not just handle) as the canonical user identifier
325- Handle browser cancellation gracefully
326
327**References:**
328- [atproto_oauth package docs](https://pub.dev/packages/atproto_oauth)
329- [flutter_web_auth_2 docs](https://pub.dev/packages/flutter_web_auth_2)
330
331#### 1.2 Implement `restoreSession()` Method
332
333**What to Implement:**
334
335```dart
336Future<OAuthSession?> restoreSession() async {
337 try {
338 final did = await _storage.read(key: _keyDid);
339
340 if (did == null) {
341 return null; // No stored session
342 }
343
344 if (_client == null) {
345 throw Exception('OAuth client not initialized');
346 }
347
348 // Check if we have a valid session for this DID
349 // The atproto_oauth package manages session storage internally
350 // We may need to use their session restoration methods
351
352 final session = await _client!.restore(did);
353
354 if (session != null) {
355 // Session still valid, return it
356 final handle = await _storage.read(key: _keyHandle);
357
358 return OAuthSession(
359 accessToken: session.accessToken,
360 refreshToken: session.refreshToken,
361 did: did,
362 handle: handle ?? '',
363 );
364 }
365
366 // Session expired, try to refresh
367 final refreshToken = await _storage.read(key: _keyRefreshToken);
368
369 if (refreshToken != null) {
370 final newSession = await _refreshSession(did, refreshToken);
371 await _storeSession(newSession);
372 return newSession;
373 }
374
375 // No valid session, user needs to log in again
376 await _clearSession();
377 return null;
378
379 } catch (e) {
380 print('Failed to restore session: $e');
381 await _clearSession();
382 return null;
383 }
384}
385```
386
387**Key Implementation Notes:**
388- Always validate stored sessions before using them
389- Attempt token refresh if access token expired but refresh token valid
390- Clear invalid sessions to prevent auth loops
391- The `atproto_oauth` package may handle session restoration internally
392
393#### 1.3 Implement Token Refresh
394
395**What to Implement:**
396
397```dart
398Future<OAuthSession> _refreshSession(String did, String refreshToken) async {
399 if (_client == null) {
400 throw Exception('OAuth client not initialized');
401 }
402
403 // Use atproto_oauth's refresh method
404 final newSession = await _client!.refresh(
405 refreshToken: refreshToken,
406 // Package handles DPoP proof for refresh
407 );
408
409 final handle = await _storage.read(key: _keyHandle);
410
411 return OAuthSession(
412 accessToken: newSession.accessToken,
413 refreshToken: newSession.refreshToken ?? refreshToken,
414 did: did,
415 handle: handle ?? '',
416 );
417}
418```
419
420#### 1.4 Implement `signOut()` Method
421
422**What to Implement:**
423
424```dart
425Future<void> signOut() async {
426 try {
427 final refreshToken = await _storage.read(key: _keyRefreshToken);
428 final did = await _storage.read(key: _keyDid);
429
430 // Revoke tokens on the authorization server
431 if (_client != null && refreshToken != null) {
432 try {
433 await _client!.revoke(
434 token: refreshToken,
435 // May need DID or other params
436 );
437 } catch (e) {
438 print('Token revocation failed (continuing with local logout): $e');
439 // Continue even if revocation fails (network issues, etc.)
440 }
441 }
442
443 // Clear local session
444 await _clearSession();
445
446 } catch (e) {
447 print('Sign out failed: $e');
448 // Always clear local session even if errors occur
449 await _clearSession();
450 }
451}
452```
453
454**Key Implementation Notes:**
455- Always attempt to revoke tokens on server
456- Don't fail if revocation fails (might be offline)
457- Always clear local storage
458
459---
460
461### Phase 2: API Integration (Medium Priority)
462
463#### 2.1 Create atProto API Client
464
465**File to Create:** `lib/services/atproto_api_service.dart`
466
467The `atproto_oauth` package provides an `OAuthSession` that can be used with the `@atproto/api` equivalent for Dart. You'll need to create an API service that uses the authenticated session.
468
469**What to Implement:**
470
471```dart
472import 'package:atproto/atproto.dart'; // If available
473import 'oauth_service.dart';
474
475class AtProtoApiService {
476 final OAuthService _oauthService;
477
478 AtProtoApiService(this._oauthService);
479
480 /// Create an authenticated API client
481 Future<ATProto?> getClient() async {
482 final session = await _oauthService.restoreSession();
483
484 if (session == null) {
485 return null;
486 }
487
488 // Create API client with session
489 // The exact API depends on available Dart atProto packages
490 return ATProto(
491 service: session.pdsUrl, // User's PDS URL
492 session: Session(
493 accessJwt: session.accessToken,
494 refreshJwt: session.refreshToken,
495 did: session.did,
496 handle: session.handle,
497 ),
498 );
499 }
500
501 /// Fetch user profile
502 Future<Profile> getProfile(String actor) async {
503 final client = await getClient();
504 if (client == null) throw Exception('Not authenticated');
505
506 return await client.getProfile(actor: actor);
507 }
508
509 /// Fetch feed
510 Future<Feed> getFeed({int limit = 50}) async {
511 final client = await getClient();
512 if (client == null) throw Exception('Not authenticated');
513
514 return await client.getTimeline(limit: limit);
515 }
516
517 /// Create post
518 Future<void> createPost(String text) async {
519 final client = await getClient();
520 if (client == null) throw Exception('Not authenticated');
521
522 await client.createRecord(
523 collection: 'app.bsky.feed.post',
524 record: {
525 'text': text,
526 'createdAt': DateTime.now().toIso8601String(),
527 },
528 );
529 }
530}
531```
532
533**Research Needed:**
534- Check if there's a Dart equivalent of `@atproto/api`
535- The `atproto_oauth` package documentation should specify how to use sessions with API calls
536- May need to create HTTP client wrapper for atProto APIs
537
538#### 2.2 Handle Token Expiration in API Calls
539
540Implement automatic token refresh when API calls fail due to expired tokens:
541
542```dart
543Future<T> _withAutoRefresh<T>(Future<T> Function() apiCall) async {
544 try {
545 return await apiCall();
546 } on UnauthorizedError {
547 // Token expired, try to refresh
548 final session = await _oauthService.restoreSession();
549 if (session == null) {
550 throw Exception('Session expired, please log in again');
551 }
552
553 // Retry API call with new token
554 return await apiCall();
555 }
556}
557```
558
559---
560
561### Phase 3: Enhanced Error Handling (Medium Priority)
562
563#### 3.1 Specific Error Types
564
565**File to Create:** `lib/models/auth_errors.dart`
566
567```dart
568class AuthError implements Exception {
569 final String message;
570 final AuthErrorType type;
571
572 AuthError(this.message, this.type);
573}
574
575enum AuthErrorType {
576 networkError,
577 invalidHandle,
578 serverError,
579 userCancelled,
580 tokenExpired,
581 unknown,
582}
583```
584
585#### 3.2 User-Friendly Error Messages
586
587Update `AuthProvider` to provide actionable error messages:
588
589```dart
590String _getErrorMessage(Exception e) {
591 if (e is AuthError) {
592 switch (e.type) {
593 case AuthErrorType.networkError:
594 return 'Unable to connect. Check your internet connection.';
595 case AuthErrorType.invalidHandle:
596 return 'Invalid handle. Use format: user.domain.com';
597 case AuthErrorType.userCancelled:
598 return 'Sign in was cancelled.';
599 case AuthErrorType.tokenExpired:
600 return 'Your session expired. Please sign in again.';
601 default:
602 return 'Sign in failed. Please try again.';
603 }
604 }
605 return e.toString();
606}
607```
608
609---
610
611### Phase 4: Session Lifecycle (Low Priority)
612
613#### 4.1 Automatic Token Refresh
614
615Implement background token refresh before expiration:
616
617```dart
618class AuthProvider with ChangeNotifier {
619 Timer? _refreshTimer;
620
621 void _scheduleTokenRefresh(DateTime expiresAt) {
622 _refreshTimer?.cancel();
623
624 // Refresh 5 minutes before expiration
625 final refreshTime = expiresAt.subtract(Duration(minutes: 5));
626 final delay = refreshTime.difference(DateTime.now());
627
628 if (delay.isNegative) {
629 _refreshTokenNow();
630 return;
631 }
632
633 _refreshTimer = Timer(delay, _refreshTokenNow);
634 }
635
636 Future<void> _refreshTokenNow() async {
637 try {
638 await _oauthService.restoreSession(); // Triggers refresh
639 } catch (e) {
640 print('Auto-refresh failed: $e');
641 }
642 }
643}
644```
645
646#### 4.2 Handle App Lifecycle
647
648React to app going to background/foreground:
649
650```dart
651class AuthProvider with ChangeNotifier, WidgetsBindingObserver {
652 @override
653 void didChangeAppLifecycleState(AppLifecycleState state) {
654 if (state == AppLifecycleState.resumed) {
655 // App came to foreground, validate session
656 _validateSession();
657 }
658 }
659
660 Future<void> _validateSession() async {
661 if (!_isAuthenticated) return;
662
663 // Check if session is still valid
664 final session = await _oauthService.restoreSession();
665 if (session == null) {
666 // Session expired while app was in background
667 _isAuthenticated = false;
668 _session = null;
669 notifyListeners();
670 }
671 }
672}
673```
674
675---
676
677## 📚 Resources & References
678
679### atProto OAuth Specifications
680- [atProto OAuth Spec](https://atproto.com/specs/oauth)
681- [DPoP (Demonstrating Proof-of-Possession)](https://datatracker.ietf.org/doc/html/rfc9449)
682- [PKCE (Proof Key for Code Exchange)](https://datatracker.ietf.org/doc/html/rfc7636)
683
684### Package Documentation
685- [`atproto_oauth` on pub.dev](https://pub.dev/packages/atproto_oauth)
686- [`flutter_web_auth_2` on pub.dev](https://pub.dev/packages/flutter_web_auth_2)
687- [`flutter_secure_storage` on pub.dev](https://pub.dev/packages/flutter_secure_storage)
688
689### Cloudflare Worker
690Your OAuth server is hosted at:
691- Base URL: `https://lingering-darkness-50a6.brettmay0212.workers.dev`
692- Client Metadata: `https://lingering-darkness-50a6.brettmay0212.workers.dev/client-metadata.json`
693- Callback: `https://lingering-darkness-50a6.brettmay0212.workers.dev/oauth/callback`
694
695---
696
697## 🧪 Testing Strategy
698
699### Unit Tests
700
701**File to Create:** `test/services/oauth_service_test.dart`
702
703```dart
704void main() {
705 group('OAuthService', () {
706 late OAuthService service;
707
708 setUp(() {
709 service = OAuthService();
710 });
711
712 test('initialize fetches client metadata', () async {
713 await service.initialize();
714 expect(service._client, isNotNull);
715 });
716
717 test('signIn returns session on success', () async {
718 final session = await service.signIn('test.bsky.social');
719 expect(session.did, isNotEmpty);
720 expect(session.accessToken, isNotEmpty);
721 });
722
723 // Add more tests...
724 });
725}
726```
727
728### Integration Tests
729
730Test the full OAuth flow on a real device:
731
7321. Open app (should show landing page)
7332. Tap "Sign in"
7343. Enter valid handle
7354. Browser opens showing PDS authorization page
7365. User authorizes
7376. App receives callback and completes sign in
7387. User is redirected to feed
7398. Close app and reopen (should restore session)
740
741---
742
743## 🔐 Security Considerations
744
745### Current Implementation ✅
746- ✅ Tokens stored in encrypted `flutter_secure_storage`
747- ✅ DPoP prevents token theft
748- ✅ PKCE prevents authorization code interception
749- ✅ HTTPS deep links preferred over custom schemes
750- ✅ Storage keys properly scoped to atProto (not app-specific)
751
752### To Verify ⚠️
753- ⚠️ Client metadata hosted securely on Cloudflare Worker
754- ⚠️ Redirect URIs match exactly (no wildcards)
755- ⚠️ Token refresh implemented securely
756- ⚠️ Session validation on app resume
757
758---
759
760## 📝 Implementation Checklist
761
762### Phase 1: Core OAuth Flow
763- [ ] Implement `signIn()` with atproto_oauth authorize flow
764- [ ] Implement callback handling and token exchange
765- [ ] Implement `restoreSession()` with validation
766- [ ] Implement token refresh logic
767- [ ] Implement `signOut()` with server-side revocation
768- [ ] Test full sign in flow on device
769- [ ] Test session restoration
770- [ ] Test sign out
771
772### Phase 2: API Integration
773- [ ] Research Dart atProto API packages
774- [ ] Create `AtProtoApiService`
775- [ ] Implement profile fetching
776- [ ] Implement feed fetching
777- [ ] Implement post creation
778- [ ] Add automatic token refresh to API calls
779
780### Phase 3: Error Handling
781- [ ] Create typed error classes
782- [ ] Add user-friendly error messages
783- [ ] Handle network errors gracefully
784- [ ] Handle authorization cancellation
785- [ ] Add error recovery flows
786
787### Phase 4: Session Lifecycle
788- [ ] Implement automatic token refresh
789- [ ] Add app lifecycle observers
790- [ ] Validate session on app resume
791- [ ] Handle session expiration gracefully
792
793### Phase 5: Testing & Polish
794- [ ] Write unit tests for OAuth service
795- [ ] Write integration tests for full flow
796- [ ] Test on slow/unstable networks
797- [ ] Test session restoration edge cases
798- [ ] Add loading indicators for all async operations
799- [ ] Add success/error feedback to user
800
801---
802
803## 🎯 Next Immediate Steps
804
8051. **Study the `atproto_oauth` Package**
806 - Read the package documentation thoroughly
807 - Look for example code or test files
808 - Understand the `authorize()` and `callback()` methods
809
8102. **Implement Basic Sign In**
811 - Start with `signIn()` method
812 - Get the authorization URL working
813 - Test browser opening and callback
814
8153. **Test on Real Device**
816 - Use your actual Bluesky handle for testing
817 - Verify deep links work correctly
818 - Check token storage
819
8204. **Implement Session Restoration**
821 - Add `restoreSession()` logic
822 - Test app restart with active session
823 - Verify token refresh works
824
825---
826
827## 💡 Tips & Best Practices
828
8291. **Always validate sessions** before making API calls
8302. **Log OAuth flows** in debug mode for troubleshooting
8313. **Handle offline gracefully** - cache data when possible
8324. **Never log tokens** - even in debug builds
8335. **Test token expiration** by manually invalidating tokens
8346. **Use atomic operations** for session updates to prevent race conditions
8357. **Clear sessions on security errors** to prevent auth loops
836
837---
838
839## 🐛 Common Issues & Solutions
840
841### Issue: "Authorization cancelled"
842**Solution:** User may have closed browser - handle gracefully, don't show error
843
844### Issue: Deep link not opening app
845**Solution:** Check AndroidManifest.xml intent filters, verify URL scheme matches exactly
846
847### Issue: "Client not initialized"
848**Solution:** Ensure `initialize()` is called before any OAuth operations
849
850### Issue: Token refresh failing
851**Solution:** Check if refresh token is still valid, may need full re-authentication
852
853---
854
855## 📞 Need Help?
856
857- **atProto Discord**: [atproto.com/community](https://atproto.com/community)
858- **Bluesky API Docs**: [docs.bsky.app](https://docs.bsky.app)
859- **Package Issues**: [atproto_oauth GitHub](https://github.com/myConsciousness/atproto.dart)
860
861---
862
863**Last Updated:** 2025-10-27
864**Flutter Version:** 3.7.2
865**Dart Version:** 3.7.2
866**Package Version:** atproto_oauth ^0.1.0