Skip to content

Commit 9285415

Browse files
committed
feat(auth): add email sign-in page
- Implemented email link sign-in flow - Added email form with validation - Navigates to code verification page
1 parent 02c2841 commit 9285415

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//
2+
// ignore_for_file: lines_longer_than_80_chars
3+
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter_bloc/flutter_bloc.dart';
6+
import 'package:go_router/go_router.dart';
7+
import 'package:ht_main/authentication/bloc/authentication_bloc.dart';
8+
import 'package:ht_main/l10n/l10n.dart';
9+
import 'package:ht_main/router/routes.dart';
10+
import 'package:ht_main/shared/constants/app_spacing.dart';
11+
12+
/// {@template email_sign_in_page}
13+
/// Page for initiating the email link sign-in process.
14+
/// Explains the passwordless flow and collects the user's email.
15+
/// {@endtemplate}
16+
class EmailSignInPage extends StatelessWidget {
17+
/// {@macro email_sign_in_page}
18+
const EmailSignInPage({
19+
required this.isLinkingContext, // Accept the flag
20+
super.key,
21+
});
22+
23+
/// Whether this page is being shown in the account linking context.
24+
final bool isLinkingContext;
25+
26+
@override
27+
Widget build(BuildContext context) {
28+
// Assuming AuthenticationBloc is provided by the parent route (AuthenticationPage)
29+
// If not, it needs to be provided here or higher up.
30+
// Pass the flag down to the view.
31+
return _EmailSignInView(isLinkingContext: isLinkingContext);
32+
}
33+
}
34+
35+
class _EmailSignInView extends StatelessWidget {
36+
// Accept the flag from the parent page.
37+
const _EmailSignInView({required this.isLinkingContext});
38+
39+
final bool isLinkingContext;
40+
41+
@override
42+
Widget build(BuildContext context) {
43+
final l10n = context.l10n;
44+
final colorScheme = Theme.of(context).colorScheme;
45+
46+
return Scaffold(
47+
appBar: AppBar(
48+
title: Text(l10n.emailSignInPageTitle),
49+
// Add a custom leading back button to control navigation based on context.
50+
leading: IconButton(
51+
icon: const Icon(Icons.arrow_back),
52+
tooltip:
53+
MaterialLocalizations.of(
54+
context,
55+
).backButtonTooltip, // Accessibility
56+
onPressed: () {
57+
// Navigate back differently based on the context.
58+
if (isLinkingContext) {
59+
// If linking, go back to Auth page preserving the linking query param.
60+
context.goNamed(
61+
Routes.authenticationName,
62+
queryParameters: {'context': 'linking'},
63+
);
64+
} else {
65+
// If normal sign-in, just go back to the Auth page.
66+
context.goNamed(Routes.authenticationName);
67+
}
68+
},
69+
),
70+
),
71+
body: SafeArea(
72+
child: BlocConsumer<AuthenticationBloc, AuthenticationState>(
73+
listener: (context, state) {
74+
if (state is AuthenticationFailure) {
75+
ScaffoldMessenger.of(context)
76+
..hideCurrentSnackBar()
77+
..showSnackBar(
78+
SnackBar(
79+
content: Text(state.errorMessage),
80+
backgroundColor: colorScheme.error,
81+
),
82+
);
83+
} else if (state is AuthenticationCodeSentSuccess) {
84+
// Navigate to the code verification page on success, passing the email
85+
context.goNamed(
86+
Routes.verifyCodeName,
87+
pathParameters: {'email': state.email},
88+
);
89+
}
90+
},
91+
// BuildWhen prevents unnecessary rebuilds if only listening
92+
buildWhen:
93+
(previous, current) =>
94+
current is AuthenticationInitial ||
95+
current is AuthenticationRequestCodeLoading ||
96+
current
97+
is AuthenticationFailure, // Rebuild on failure to re-enable form
98+
builder: (context, state) {
99+
final isLoading = state is AuthenticationRequestCodeLoading;
100+
101+
return Padding(
102+
padding: const EdgeInsets.all(AppSpacing.paddingLarge),
103+
child: Center(
104+
child: SingleChildScrollView(
105+
child: Column(
106+
mainAxisAlignment: MainAxisAlignment.center,
107+
crossAxisAlignment: CrossAxisAlignment.stretch,
108+
children: [
109+
// --- Hardcoded Icon ---
110+
Padding(
111+
padding: const EdgeInsets.only(
112+
bottom: AppSpacing.xl,
113+
), // Spacing below icon
114+
child: Icon(
115+
Icons.email_outlined, // Hardcoded icon
116+
size:
117+
(Theme.of(context).iconTheme.size ??
118+
AppSpacing.xl) *
119+
3.0,
120+
color: Theme.of(context).colorScheme.primary,
121+
),
122+
),
123+
const SizedBox(
124+
height: AppSpacing.lg,
125+
), // Space between icon and text
126+
// --- Explanation Text ---
127+
Text(
128+
l10n.emailSignInExplanation,
129+
style: Theme.of(context).textTheme.bodyLarge,
130+
textAlign: TextAlign.center,
131+
),
132+
const SizedBox(height: AppSpacing.xxl),
133+
_EmailLinkForm(isLoading: isLoading),
134+
],
135+
),
136+
),
137+
),
138+
);
139+
},
140+
),
141+
),
142+
);
143+
}
144+
}
145+
146+
/// --- Reusable Email Form Widget --- ///
147+
148+
class _EmailLinkForm extends StatefulWidget {
149+
const _EmailLinkForm({required this.isLoading});
150+
151+
final bool isLoading;
152+
153+
@override
154+
State<_EmailLinkForm> createState() => _EmailLinkFormState();
155+
}
156+
157+
class _EmailLinkFormState extends State<_EmailLinkForm> {
158+
final _emailController = TextEditingController();
159+
final _formKey = GlobalKey<FormState>();
160+
161+
@override
162+
void dispose() {
163+
_emailController.dispose();
164+
super.dispose();
165+
}
166+
167+
void _submitForm() {
168+
if (_formKey.currentState!.validate()) {
169+
context.read<AuthenticationBloc>().add(
170+
AuthenticationRequestSignInCodeRequested(
171+
email: _emailController.text.trim(),
172+
),
173+
);
174+
}
175+
}
176+
177+
@override
178+
Widget build(BuildContext context) {
179+
final l10n = context.l10n;
180+
181+
return Form(
182+
key: _formKey,
183+
child: Column(
184+
crossAxisAlignment: CrossAxisAlignment.stretch,
185+
children: [
186+
TextFormField(
187+
controller: _emailController,
188+
decoration: InputDecoration(
189+
labelText: l10n.authenticationEmailLabel,
190+
border: const OutlineInputBorder(),
191+
// Consider adding hint text if needed
192+
),
193+
keyboardType: TextInputType.emailAddress,
194+
autocorrect: false,
195+
textInputAction: TextInputAction.done,
196+
enabled: !widget.isLoading,
197+
validator: (value) {
198+
if (value == null || value.isEmpty || !value.contains('@')) {
199+
return l10n.accountLinkingEmailValidationError;
200+
}
201+
return null;
202+
},
203+
onFieldSubmitted:
204+
(_) => _submitForm(), // Allow submitting from keyboard
205+
),
206+
const SizedBox(height: AppSpacing.lg),
207+
ElevatedButton(
208+
onPressed: widget.isLoading ? null : _submitForm,
209+
child:
210+
widget.isLoading
211+
? const SizedBox(
212+
height: 24,
213+
width: 24,
214+
child: CircularProgressIndicator(strokeWidth: 2),
215+
)
216+
: Text(l10n.authenticationSendLinkButton),
217+
),
218+
],
219+
),
220+
);
221+
}
222+
}

0 commit comments

Comments
 (0)