Skip to content

Commit 66be3f2

Browse files
committed
feat(auth): implement email sign-in flow
- Added email sign-in page - Added email link sent page - Updated routing and navigation - Refactored auth page for linking
1 parent 4f38bb0 commit 66be3f2

File tree

8 files changed

+607
-206
lines changed

8 files changed

+607
-206
lines changed

lib/account/view/account_page.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,18 +156,21 @@ class _AccountView extends StatelessWidget {
156156
);
157157
}
158158

159-
/// Builds the ListTile for navigating to Backup/Account Linking (for anonymous users).
159+
/// Builds the ListTile prompting anonymous users to sign in/connect.
160160
Widget _buildBackupTile(BuildContext context) {
161161
final l10n = context.l10n;
162162
return ListTile(
163-
leading: const Icon(
164-
Icons.cloud_upload_outlined,
165-
), // Or Icons.link, Icons.save
166-
title: Text(l10n.accountBackupTile),
163+
leading: const Icon(Icons.link), // Icon suggesting connection/linking
164+
title: Text(l10n.accountConnectPrompt), // New l10n key needed
165+
subtitle: Text(l10n.accountConnectBenefit), // New l10n key needed
166+
isThreeLine: true, // Allow more space for subtitle
167167
trailing: const Icon(Icons.chevron_right),
168168
onTap: () {
169-
// Navigate to the account linking page
170-
context.goNamed(Routes.accountLinkingName); // Updated route name
169+
// Navigate to the authentication page in linking mode
170+
context.goNamed(
171+
Routes.authenticationName,
172+
queryParameters: {'context': 'linking'},
173+
);
171174
},
172175
);
173176
}
Lines changed: 123 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,188 +1,176 @@
1-
//
21
// ignore_for_file: lines_longer_than_80_chars
32

43
import 'package:flutter/material.dart';
54
import 'package:flutter_bloc/flutter_bloc.dart';
5+
import 'package:go_router/go_router.dart';
66
import 'package:ht_authentication_repository/ht_authentication_repository.dart';
77
import 'package:ht_main/authentication/bloc/authentication_bloc.dart';
8-
import 'package:ht_main/l10n/l10n.dart'; // Added import
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'; // Use shared constants
911

12+
/// {@template authentication_page}
13+
/// Displays authentication options (Google, Email, Anonymous) based on context.
14+
///
15+
/// This page can be used for both initial sign-in and for connecting an
16+
/// existing anonymous account.
17+
/// {@endtemplate}
1018
class AuthenticationPage extends StatelessWidget {
11-
const AuthenticationPage({super.key});
19+
/// {@macro authentication_page}
20+
const AuthenticationPage({
21+
required this.headline,
22+
required this.subHeadline,
23+
required this.showAnonymousButton,
24+
super.key,
25+
});
26+
27+
/// The main title displayed on the page.
28+
final String headline;
29+
30+
/// The descriptive text displayed below the headline.
31+
final String subHeadline;
32+
33+
/// Whether to show the "Continue Anonymously" button.
34+
final bool showAnonymousButton;
1235

1336
@override
1437
Widget build(BuildContext context) {
38+
// Provide the BLoC here if it's not already provided higher up
39+
// For this refactor, assuming it's provided by the route or App setup
1540
return BlocProvider(
16-
create:
17-
(context) => AuthenticationBloc(
18-
authenticationRepository:
19-
context.read<HtAuthenticationRepository>(),
20-
),
21-
child: _AuthenticationView(),
41+
// Ensure BLoC is created only once per instance of this page if needed
42+
// If BLoC needs to persist across navigations, provide it higher up.
43+
create: (context) => AuthenticationBloc(
44+
authenticationRepository: context.read<HtAuthenticationRepository>(),
45+
),
46+
child: _AuthenticationView(
47+
headline: headline,
48+
subHeadline: subHeadline,
49+
showAnonymousButton: showAnonymousButton,
50+
),
2251
);
2352
}
2453
}
2554

26-
class _AuthenticationView extends StatefulWidget {
27-
@override
28-
__AuthenticationViewState createState() => __AuthenticationViewState();
29-
}
30-
31-
class __AuthenticationViewState extends State<_AuthenticationView> {
32-
final _emailController = TextEditingController();
33-
// Removed password controller
55+
// Renamed from _AuthenticationView to follow convention
56+
class _AuthenticationView extends StatelessWidget {
57+
const _AuthenticationView({
58+
required this.headline,
59+
required this.subHeadline,
60+
required this.showAnonymousButton,
61+
});
3462

35-
@override
36-
void dispose() {
37-
_emailController.dispose();
38-
// Removed password controller disposal
39-
super.dispose();
40-
}
63+
final String headline;
64+
final String subHeadline;
65+
final bool showAnonymousButton;
4166

4267
@override
4368
Widget build(BuildContext context) {
44-
// Use BlocConsumer to listen for state changes for side effects (SnackBar)
69+
final l10n = context.l10n;
70+
final textTheme = Theme.of(context).textTheme;
71+
final colorScheme = Theme.of(context).colorScheme;
72+
4573
return Scaffold(
74+
// Consider adding an AppBar if needed for context/navigation
4675
body: SafeArea(
4776
child: BlocConsumer<AuthenticationBloc, AuthenticationState>(
77+
// Listener remains crucial for feedback (errors)
4878
listener: (context, state) {
4979
if (state is AuthenticationFailure) {
5080
ScaffoldMessenger.of(context)
5181
..hideCurrentSnackBar()
5282
..showSnackBar(
5383
SnackBar(
54-
content: Text(state.errorMessage),
55-
backgroundColor: Theme.of(context).colorScheme.error,
56-
),
57-
);
58-
} else if (state is AuthenticationLinkSentSuccess) {
59-
ScaffoldMessenger.of(context)
60-
..hideCurrentSnackBar()
61-
..showSnackBar(
62-
SnackBar(
63-
content: Text(context.l10n.authenticationEmailSentSuccess),
84+
content: Text(
85+
// Provide a more user-friendly error message if possible
86+
state.errorMessage, // Or map specific errors
87+
),
88+
backgroundColor: colorScheme.error,
6489
),
6590
);
66-
// Optionally clear email field or navigate
6791
}
92+
// Success states (Google/Anonymous) are typically handled by
93+
// the AppBloc listening to repository changes and triggering redirects.
94+
// Email link success is handled in the dedicated email flow pages.
6895
},
6996
builder: (context, state) {
70-
// Determine if loading indicator should be shown
71-
final isLoading =
72-
state is AuthenticationLoading ||
73-
state is AuthenticationLinkSending;
74-
final l10n = context.l10n; // Added l10n variable
97+
final isLoading = state is AuthenticationLoading; // Simplified loading check
7598

7699
return Padding(
77-
padding: const EdgeInsets.all(16), // Use AppSpacing later
100+
padding: const EdgeInsets.all(AppSpacing.paddingLarge), // Use constant
78101
child: Center(
79-
// Center content vertically
80102
child: SingleChildScrollView(
81-
// Allow scrolling if needed
82103
child: Column(
83-
// Use CrossAxisAlignment.stretch for full-width buttons
104+
mainAxisAlignment: MainAxisAlignment.center, // Center vertically
84105
crossAxisAlignment: CrossAxisAlignment.stretch,
85106
children: [
107+
// --- Headline and Subheadline ---
86108
Text(
87-
l10n.authenticationPageTitle,
88-
style:
89-
Theme.of(
90-
context,
91-
).textTheme.headlineMedium, // Use theme typography
109+
headline,
110+
style: textTheme.headlineMedium,
92111
textAlign: TextAlign.center,
93112
),
94-
const SizedBox(height: 32), // Use AppSpacing later
95-
TextFormField(
96-
controller: _emailController,
97-
decoration: InputDecoration(
98-
labelText: l10n.authenticationEmailLabel,
99-
border: const OutlineInputBorder(),
100-
),
101-
keyboardType: TextInputType.emailAddress,
102-
autocorrect: false,
103-
textInputAction:
104-
TextInputAction.done, // Improve keyboard action
105-
enabled: !isLoading, // Disable field when loading
106-
),
107-
// Removed Password Field
108-
const SizedBox(height: 32), // Use AppSpacing later
109-
// Show loading indicator within the button if sending link
110-
ElevatedButton(
111-
onPressed:
112-
isLoading // Disable button when loading
113-
? null
114-
: () {
115-
context.read<AuthenticationBloc>().add(
116-
AuthenticationSendSignInLinkRequested(
117-
email:
118-
_emailController.text
119-
.trim(), // Trim whitespace
120-
),
121-
);
122-
},
123-
child:
124-
state is AuthenticationLinkSending
125-
? const SizedBox(
126-
// Consistent height loading indicator
127-
height: 24,
128-
width: 24,
129-
child: CircularProgressIndicator(
130-
strokeWidth: 2,
131-
),
132-
)
133-
: Text(l10n.authenticationSendLinkButton),
113+
const SizedBox(height: AppSpacing.sm), // Use constant
114+
Text(
115+
subHeadline,
116+
style: textTheme.bodyLarge,
117+
textAlign: TextAlign.center,
134118
),
135-
const SizedBox(height: 16), // Use AppSpacing later
136-
// Add divider for clarity
137-
Row(
138-
// Removed const
139-
children: [
140-
const Expanded(
141-
child: Divider(),
142-
), // Added const back here
143-
Padding(
144-
padding: const EdgeInsets.symmetric(horizontal: 8),
145-
child: Text(l10n.authenticationOrDivider),
146-
),
147-
const Expanded(child: Divider()),
148-
],
119+
const SizedBox(height: AppSpacing.xxl), // Use constant
120+
121+
// --- Google Sign-In Button ---
122+
ElevatedButton.icon(
123+
icon: const Icon(Icons.g_mobiledata), // Placeholder icon
124+
label: Text(l10n.authenticationGoogleSignInButton),
125+
onPressed: isLoading
126+
? null
127+
: () => context.read<AuthenticationBloc>().add(
128+
const AuthenticationGoogleSignInRequested(),
129+
),
130+
// Style adjustments can be made via ElevatedButtonThemeData
149131
),
150-
const SizedBox(height: 16), // Use AppSpacing later
132+
const SizedBox(height: AppSpacing.lg), // Use constant
133+
134+
// --- Email Sign-In Button ---
151135
ElevatedButton(
152-
// Removed duplicate onPressed here
153-
// Style adjustments for Google button might be needed via Theme
154-
onPressed:
155-
isLoading // Disable button when loading
156-
? null
157-
: () {
158-
context.read<AuthenticationBloc>().add(
159-
const AuthenticationGoogleSignInRequested(),
160-
);
161-
},
162-
// Consider adding Google icon
163-
child: Text(l10n.authenticationGoogleSignInButton),
136+
// Consider an email icon
137+
// icon: const Icon(Icons.email_outlined),
138+
child: Text(l10n.authenticationEmailSignInButton), // New l10n key needed
139+
onPressed: isLoading
140+
? null
141+
: () {
142+
// Navigate to the dedicated email sign-in page
143+
context.goNamed(Routes.emailSignInName);
144+
},
164145
),
165-
const SizedBox(height: 16), // Use AppSpacing later
166-
OutlinedButton(
167-
// Use OutlinedButton for less emphasis
168-
onPressed:
169-
isLoading // Disable button when loading
170-
? null
171-
: () {
172-
context.read<AuthenticationBloc>().add(
146+
147+
// --- Anonymous Sign-In Button (Conditional) ---
148+
if (showAnonymousButton) ...[
149+
const SizedBox(height: AppSpacing.lg), // Use constant
150+
OutlinedButton(
151+
child: Text(l10n.authenticationAnonymousSignInButton),
152+
onPressed: isLoading
153+
? null
154+
: () => context.read<AuthenticationBloc>().add(
173155
const AuthenticationAnonymousSignInRequested(),
174-
);
175-
},
176-
child: Text(l10n.authenticationAnonymousSignInButton),
177-
),
156+
),
157+
),
158+
],
159+
160+
// --- Loading Indicator (Optional, for general loading state) ---
161+
// If needed, show a general loading indicator when state is AuthenticationLoading
162+
if (isLoading && state is! AuthenticationLinkSending) ...[
163+
const SizedBox(height: AppSpacing.xl),
164+
const Center(child: CircularProgressIndicator()),
165+
]
178166
],
179-
), // Column
180-
), // SingleChildScrollView
181-
), // Center
182-
); // Padding
167+
),
168+
),
169+
),
170+
);
183171
},
184-
), // BlocConsumer
185-
), // SafeArea
186-
); // Scaffold
172+
),
173+
),
174+
);
187175
}
188176
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:ht_main/l10n/l10n.dart';
3+
import 'package:ht_main/shared/constants/app_spacing.dart';
4+
5+
/// {@template email_link_sent_page}
6+
/// Confirmation page shown after a sign-in link has been sent to the user's email.
7+
/// Instructs the user to check their inbox.
8+
/// {@endtemplate}
9+
class EmailLinkSentPage extends StatelessWidget {
10+
/// {@macro email_link_sent_page}
11+
const EmailLinkSentPage({super.key});
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
final l10n = context.l10n;
16+
final textTheme = Theme.of(context).textTheme;
17+
18+
return Scaffold(
19+
appBar: AppBar(
20+
title: Text(l10n.emailLinkSentPageTitle), // New l10n key needed
21+
),
22+
body: SafeArea(
23+
child: Padding(
24+
padding: const EdgeInsets.all(AppSpacing.paddingLarge),
25+
child: Center(
26+
child: Column(
27+
mainAxisAlignment: MainAxisAlignment.center,
28+
crossAxisAlignment: CrossAxisAlignment.center,
29+
children: [
30+
const Icon(
31+
Icons.mark_email_read_outlined, // Suggestive icon
32+
size: 80,
33+
// Consider using theme color
34+
// color: Theme.of(context).colorScheme.primary,
35+
),
36+
const SizedBox(height: AppSpacing.xl),
37+
Text(
38+
l10n.emailLinkSentConfirmation, // New l10n key needed
39+
style: textTheme.titleLarge, // Prominent text style
40+
textAlign: TextAlign.center,
41+
),
42+
// Optional: Add a button to go back if needed,
43+
// but AppBar back button might suffice.
44+
// const SizedBox(height: AppSpacing.xxl),
45+
// OutlinedButton(
46+
// onPressed: () => context.pop(), // Or navigate elsewhere
47+
// child: Text(l10n.backButtonLabel), // New l10n key
48+
// ),
49+
],
50+
),
51+
),
52+
),
53+
),
54+
);
55+
}
56+
}

0 commit comments

Comments
 (0)