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