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