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