1import 'package:flutter/material.dart'; 2import 'package:go_router/go_router.dart'; 3import 'package:provider/provider.dart'; 4 5import '../../providers/auth_provider.dart'; 6import '../../widgets/primary_button.dart'; 7 8class LoginScreen extends StatefulWidget { 9 const LoginScreen({super.key}); 10 11 @override 12 State<LoginScreen> createState() => _LoginScreenState(); 13} 14 15class _LoginScreenState extends State<LoginScreen> { 16 final _handleController = TextEditingController(); 17 final _formKey = GlobalKey<FormState>(); 18 bool _isLoading = false; 19 20 @override 21 void dispose() { 22 _handleController.dispose(); 23 super.dispose(); 24 } 25 26 Future<void> _handleSignIn() async { 27 if (!_formKey.currentState!.validate()) { 28 return; 29 } 30 31 setState(() => _isLoading = true); 32 33 try { 34 final authProvider = Provider.of<AuthProvider>(context, listen: false); 35 await authProvider.signIn(_handleController.text.trim()); 36 37 if (mounted) { 38 // Navigate to feed on successful login 39 context.go('/feed'); 40 } 41 } on Exception catch (e) { 42 if (mounted) { 43 ScaffoldMessenger.of(context).showSnackBar( 44 SnackBar( 45 content: Text('Sign in failed: ${e.toString()}'), 46 backgroundColor: Colors.red[700], 47 ), 48 ); 49 } 50 } finally { 51 if (mounted) { 52 setState(() => _isLoading = false); 53 } 54 } 55 } 56 57 @override 58 Widget build(BuildContext context) { 59 return PopScope( 60 canPop: false, 61 onPopInvokedWithResult: (didPop, result) { 62 if (!didPop) { 63 context.go('/'); 64 } 65 }, 66 child: Scaffold( 67 backgroundColor: const Color(0xFF0B0F14), 68 appBar: AppBar( 69 backgroundColor: const Color(0xFF0B0F14), 70 foregroundColor: Colors.white, 71 title: const Text('Sign In'), 72 elevation: 0, 73 leading: IconButton( 74 icon: const Icon(Icons.arrow_back), 75 onPressed: () => context.go('/'), 76 ), 77 ), 78 body: SafeArea( 79 child: Padding( 80 padding: const EdgeInsets.all(24), 81 child: Form( 82 key: _formKey, 83 child: Column( 84 crossAxisAlignment: CrossAxisAlignment.stretch, 85 children: [ 86 const SizedBox(height: 32), 87 88 // Title 89 const Text( 90 'Enter your handle', 91 style: TextStyle( 92 fontSize: 24, 93 color: Colors.white, 94 fontWeight: FontWeight.bold, 95 ), 96 textAlign: TextAlign.center, 97 ), 98 99 const SizedBox(height: 8), 100 101 // Subtitle 102 const Text( 103 'Sign in with your atProto handle to continue', 104 style: TextStyle(fontSize: 16, color: Color(0xFFB6C2D2)), 105 textAlign: TextAlign.center, 106 ), 107 108 const SizedBox(height: 48), 109 110 // Handle input field 111 TextFormField( 112 controller: _handleController, 113 enabled: !_isLoading, 114 style: const TextStyle(color: Colors.white), 115 decoration: InputDecoration( 116 hintText: 'alice.bsky.social', 117 hintStyle: const TextStyle(color: Color(0xFF5A6B7F)), 118 filled: true, 119 fillColor: const Color(0xFF1A2028), 120 border: OutlineInputBorder( 121 borderRadius: BorderRadius.circular(12), 122 borderSide: const BorderSide(color: Color(0xFF2A3441)), 123 ), 124 enabledBorder: OutlineInputBorder( 125 borderRadius: BorderRadius.circular(12), 126 borderSide: const BorderSide(color: Color(0xFF2A3441)), 127 ), 128 focusedBorder: OutlineInputBorder( 129 borderRadius: BorderRadius.circular(12), 130 borderSide: const BorderSide( 131 color: Color(0xFFFF6B35), 132 width: 2, 133 ), 134 ), 135 prefixIcon: const Icon( 136 Icons.person, 137 color: Color(0xFF5A6B7F), 138 ), 139 ), 140 keyboardType: TextInputType.emailAddress, 141 autocorrect: false, 142 textInputAction: TextInputAction.done, 143 onFieldSubmitted: (_) => _handleSignIn(), 144 validator: (value) { 145 if (value == null || value.trim().isEmpty) { 146 return 'Please enter your handle'; 147 } 148 // Basic handle validation 149 if (!value.contains('.')) { 150 return 'Handle must contain a domain ' 151 '(e.g., user.bsky.social)'; 152 } 153 return null; 154 }, 155 ), 156 157 const SizedBox(height: 32), 158 159 // Sign in button 160 PrimaryButton( 161 title: _isLoading ? 'Signing in...' : 'Sign In', 162 onPressed: _isLoading ? () {} : _handleSignIn, 163 disabled: _isLoading, 164 ), 165 166 const SizedBox(height: 24), 167 168 // Info text 169 const Text( 170 'You\'ll be redirected to authorize this app with your ' 171 'atProto provider.', 172 style: TextStyle(fontSize: 14, color: Color(0xFF5A6B7F)), 173 textAlign: TextAlign.center, 174 ), 175 176 const Spacer(), 177 178 // Help text 179 Center( 180 child: TextButton( 181 onPressed: () { 182 showDialog( 183 context: context, 184 builder: 185 (context) => AlertDialog( 186 backgroundColor: const Color(0xFF1A2028), 187 title: const Text( 188 'What is a handle?', 189 style: TextStyle(color: Colors.white), 190 ), 191 content: const Text( 192 'Your handle is your unique identifier ' 193 'on the atProto network, like ' 194 'alice.bsky.social. If you don\'t have one ' 195 'yet, you can create an account at bsky.app.', 196 style: TextStyle(color: Color(0xFFB6C2D2)), 197 ), 198 actions: [ 199 TextButton( 200 onPressed: 201 () => Navigator.of(context).pop(), 202 child: const Text('Got it'), 203 ), 204 ], 205 ), 206 ); 207 }, 208 child: const Text( 209 'What is a handle?', 210 style: TextStyle( 211 color: Color(0xFFFF6B35), 212 decoration: TextDecoration.underline, 213 ), 214 ), 215 ), 216 ), 217 ], 218 ), 219 ), 220 ), 221 ), 222 ), 223 ); 224 } 225}