fix: implement auth-based routing and adaptive feed screen

- Add redirect logic to auto-route authenticated users to /feed on app restart
- Allow anonymous users to browse /feed with public explore view
- Explicitly redirect to landing screen on sign-out
- Make FeedScreen adaptive: personalized view for authenticated users, explore view for anonymous users
- Remove unused atproto_oauth_flutter import from main.dart

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+146 -65
lib
+61 -36
lib/main.dart
···
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
-
import 'package:atproto_oauth_flutter/atproto_oauth_flutter.dart';
import 'screens/landing_screen.dart';
import 'screens/auth/login_screen.dart';
import 'screens/home/feed_screen.dart';
···
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Coves',
theme: ThemeData(
···
),
useMaterial3: true,
),
-
routerConfig: _router,
debugShowCheckedModeBanner: false,
);
}
}
-
// GoRouter configuration
-
final _router = GoRouter(
-
routes: [
-
GoRoute(
-
path: '/',
-
builder: (context, state) => const LandingScreen(),
-
),
-
GoRoute(
-
path: '/login',
-
builder: (context, state) => const LoginScreen(),
-
),
-
GoRoute(
-
path: '/feed',
-
builder: (context, state) => const FeedScreen(),
-
),
-
],
-
// No custom redirect - let errorBuilder handle OAuth callbacks
-
errorBuilder: (context, state) {
-
// Check if this is an OAuth callback
-
if (state.uri.scheme == OAuthConfig.customScheme) {
-
if (kDebugMode) {
-
print('⚠️ OAuth callback in errorBuilder - flutter_web_auth_2 should handle it');
-
print(' URI: ${state.uri}');
}
-
// Return nothing - just stay on current screen
-
// flutter_web_auth_2 will process the callback at native level
-
return const SizedBox.shrink();
-
}
-
// For other errors, show landing page
-
if (kDebugMode) {
-
print('⚠️ Router error: ${state.uri}');
-
}
-
return const LandingScreen();
-
},
-
);
···
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'screens/landing_screen.dart';
import 'screens/auth/login_screen.dart';
import 'screens/home/feed_screen.dart';
···
@override
Widget build(BuildContext context) {
+
final authProvider = Provider.of<AuthProvider>(context, listen: false);
+
return MaterialApp.router(
title: 'Coves',
theme: ThemeData(
···
),
useMaterial3: true,
),
+
routerConfig: _createRouter(authProvider),
debugShowCheckedModeBanner: false,
);
}
}
+
// GoRouter configuration factory
+
GoRouter _createRouter(AuthProvider authProvider) {
+
return GoRouter(
+
routes: [
+
GoRoute(
+
path: '/',
+
builder: (context, state) => const LandingScreen(),
+
),
+
GoRoute(
+
path: '/login',
+
builder: (context, state) => const LoginScreen(),
+
),
+
GoRoute(
+
path: '/feed',
+
builder: (context, state) => const FeedScreen(),
+
),
+
],
+
refreshListenable: authProvider,
+
redirect: (context, state) {
+
final isAuthenticated = authProvider.isAuthenticated;
+
final isLoading = authProvider.isLoading;
+
final currentPath = state.uri.path;
+
+
// Don't redirect while loading initial auth state
+
if (isLoading) {
+
return null;
+
}
+
+
// If authenticated and on landing/login screen, redirect to feed
+
if (isAuthenticated && (currentPath == '/' || currentPath == '/login')) {
+
if (kDebugMode) {
+
print('🔄 User authenticated, redirecting to /feed');
+
}
+
return '/feed';
+
}
+
+
// Allow anonymous users to access /feed for browsing
+
// Sign-out redirect is handled explicitly in the sign-out action
+
return null;
+
},
+
errorBuilder: (context, state) {
+
// Check if this is an OAuth callback
+
if (state.uri.scheme == OAuthConfig.customScheme) {
+
if (kDebugMode) {
+
print('⚠️ OAuth callback in errorBuilder - flutter_web_auth_2 should handle it');
+
print(' URI: ${state.uri}');
+
}
+
// Return nothing - just stay on current screen
+
// flutter_web_auth_2 will process the callback at native level
+
return const SizedBox.shrink();
}
+
// For other errors, show landing page
+
if (kDebugMode) {
+
print('⚠️ Router error: ${state.uri}');
+
}
+
return const LandingScreen();
+
},
+
);
+
}
+85 -29
lib/screens/home/feed_screen.dart
···
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/primary_button.dart';
class FeedScreen extends StatelessWidget {
···
@override
Widget build(BuildContext context) {
-
return PopScope(
-
canPop: false,
-
onPopInvokedWithResult: (didPop, result) {
-
if (!didPop) {
-
context.go('/');
-
}
-
},
-
child: Scaffold(
backgroundColor: const Color(0xFF0B0F14),
appBar: AppBar(
backgroundColor: const Color(0xFF0B0F14),
foregroundColor: Colors.white,
-
title: const Text('Feed'),
-
leading: IconButton(
-
icon: const Icon(Icons.arrow_back),
-
onPressed: () => context.go('/'),
-
),
),
body: Center(
child: Padding(
···
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
-
const Text(
-
'Feed Screen',
-
style: TextStyle(
-
fontSize: 24,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
-
const Text(
-
'Browse communities without signing in',
-
style: TextStyle(
fontSize: 16,
color: Color(0xFFB6C2D2),
),
textAlign: TextAlign.center,
),
-
const SizedBox(height: 32),
-
PrimaryButton(
-
title: 'Back to Home',
-
onPressed: () {
-
context.go('/');
-
},
-
variant: ButtonVariant.outline,
-
),
],
),
),
-
),
),
);
}
···
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
+
import 'package:provider/provider.dart';
+
import '../../providers/auth_provider.dart';
import '../../widgets/primary_button.dart';
class FeedScreen extends StatelessWidget {
···
@override
Widget build(BuildContext context) {
+
final authProvider = Provider.of<AuthProvider>(context);
+
final isAuthenticated = authProvider.isAuthenticated;
+
+
return Scaffold(
backgroundColor: const Color(0xFF0B0F14),
appBar: AppBar(
backgroundColor: const Color(0xFF0B0F14),
foregroundColor: Colors.white,
+
title: Text(isAuthenticated ? 'Feed' : 'Explore'),
+
automaticallyImplyLeading: !isAuthenticated,
+
leading: !isAuthenticated
+
? IconButton(
+
icon: const Icon(Icons.arrow_back),
+
onPressed: () => context.go('/'),
+
)
+
: null,
+
actions: isAuthenticated
+
? [
+
IconButton(
+
icon: const Icon(Icons.person),
+
onPressed: () {
+
// TODO: Navigate to profile screen
+
},
+
),
+
]
+
: null,
),
body: Center(
child: Padding(
···
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
+
const Icon(
+
Icons.forum,
+
size: 64,
+
color: Color(0xFFFF6B35),
+
),
+
const SizedBox(height: 24),
+
Text(
+
isAuthenticated ? 'Welcome to Coves!' : 'Explore Coves',
+
style: const TextStyle(
+
fontSize: 28,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
+
if (isAuthenticated && authProvider.did != null) ...[
+
Text(
+
'Signed in as:',
+
style: TextStyle(
+
fontSize: 14,
+
color: Colors.white.withValues(alpha: 0.6),
+
),
+
),
+
const SizedBox(height: 4),
+
Text(
+
authProvider.did!,
+
style: const TextStyle(
+
fontSize: 16,
+
color: Color(0xFFB6C2D2),
+
fontFamily: 'monospace',
+
),
+
textAlign: TextAlign.center,
+
),
+
],
+
const SizedBox(height: 32),
+
Text(
+
isAuthenticated
+
? 'Your personalized feed will appear here'
+
: 'Browse communities and discover conversations',
+
style: const TextStyle(
fontSize: 16,
color: Color(0xFFB6C2D2),
),
textAlign: TextAlign.center,
),
+
const SizedBox(height: 48),
+
if (isAuthenticated) ...[
+
PrimaryButton(
+
title: 'Sign Out',
+
onPressed: () async {
+
await authProvider.signOut();
+
// Explicitly redirect to landing screen after sign out
+
if (context.mounted) {
+
context.go('/');
+
}
+
},
+
variant: ButtonVariant.outline,
+
),
+
] else ...[
+
PrimaryButton(
+
title: 'Sign in',
+
onPressed: () => context.go('/login'),
+
variant: ButtonVariant.solid,
+
),
+
const SizedBox(height: 12),
+
PrimaryButton(
+
title: 'Create account',
+
onPressed: () => context.go('/login'),
+
variant: ButtonVariant.outline,
+
),
+
],
],
),
),
),
);
}