|
1 |
| -// |
2 | 1 | // ignore_for_file: lines_longer_than_80_chars
|
3 | 2 |
|
4 | 3 | import 'package:flutter/material.dart';
|
5 | 4 | import 'package:flutter_bloc/flutter_bloc.dart';
|
| 5 | +import 'package:go_router/go_router.dart'; |
6 | 6 | import 'package:ht_authentication_repository/ht_authentication_repository.dart';
|
7 | 7 | 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 |
9 | 11 |
|
| 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} |
10 | 18 | 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; |
12 | 35 |
|
13 | 36 | @override
|
14 | 37 | 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 |
15 | 40 | 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 | + ), |
22 | 51 | );
|
23 | 52 | }
|
24 | 53 | }
|
25 | 54 |
|
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 | + }); |
34 | 62 |
|
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; |
41 | 66 |
|
42 | 67 | @override
|
43 | 68 | 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 | + |
45 | 73 | return Scaffold(
|
| 74 | + // Consider adding an AppBar if needed for context/navigation |
46 | 75 | body: SafeArea(
|
47 | 76 | child: BlocConsumer<AuthenticationBloc, AuthenticationState>(
|
| 77 | + // Listener remains crucial for feedback (errors) |
48 | 78 | listener: (context, state) {
|
49 | 79 | if (state is AuthenticationFailure) {
|
50 | 80 | ScaffoldMessenger.of(context)
|
51 | 81 | ..hideCurrentSnackBar()
|
52 | 82 | ..showSnackBar(
|
53 | 83 | 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, |
64 | 89 | ),
|
65 | 90 | );
|
66 |
| - // Optionally clear email field or navigate |
67 | 91 | }
|
| 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. |
68 | 95 | },
|
69 | 96 | 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 |
75 | 98 |
|
76 | 99 | return Padding(
|
77 |
| - padding: const EdgeInsets.all(16), // Use AppSpacing later |
| 100 | + padding: const EdgeInsets.all(AppSpacing.paddingLarge), // Use constant |
78 | 101 | child: Center(
|
79 |
| - // Center content vertically |
80 | 102 | child: SingleChildScrollView(
|
81 |
| - // Allow scrolling if needed |
82 | 103 | child: Column(
|
83 |
| - // Use CrossAxisAlignment.stretch for full-width buttons |
| 104 | + mainAxisAlignment: MainAxisAlignment.center, // Center vertically |
84 | 105 | crossAxisAlignment: CrossAxisAlignment.stretch,
|
85 | 106 | children: [
|
| 107 | + // --- Headline and Subheadline --- |
86 | 108 | Text(
|
87 |
| - l10n.authenticationPageTitle, |
88 |
| - style: |
89 |
| - Theme.of( |
90 |
| - context, |
91 |
| - ).textTheme.headlineMedium, // Use theme typography |
| 109 | + headline, |
| 110 | + style: textTheme.headlineMedium, |
92 | 111 | textAlign: TextAlign.center,
|
93 | 112 | ),
|
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, |
134 | 118 | ),
|
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 |
149 | 131 | ),
|
150 |
| - const SizedBox(height: 16), // Use AppSpacing later |
| 132 | + const SizedBox(height: AppSpacing.lg), // Use constant |
| 133 | + |
| 134 | + // --- Email Sign-In Button --- |
151 | 135 | 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 | + }, |
164 | 145 | ),
|
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( |
173 | 155 | 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 | + ] |
178 | 166 | ],
|
179 |
| - ), // Column |
180 |
| - ), // SingleChildScrollView |
181 |
| - ), // Center |
182 |
| - ); // Padding |
| 167 | + ), |
| 168 | + ), |
| 169 | + ), |
| 170 | + ); |
183 | 171 | },
|
184 |
| - ), // BlocConsumer |
185 |
| - ), // SafeArea |
186 |
| - ); // Scaffold |
| 172 | + ), |
| 173 | + ), |
| 174 | + ); |
187 | 175 | }
|
188 | 176 | }
|
0 commit comments