Skip to content

Commit 16024dd

Browse files
authored
Merge pull request #34 from flutter-news-app-full-source-code/enhance_auth_feature
Enhance auth feature
2 parents 4affd9f + 66b8797 commit 16024dd

13 files changed

+243
-75
lines changed

lib/authentication/bloc/authentication_bloc.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import 'package:equatable/equatable.dart';
2020
part 'authentication_event.dart';
2121
part 'authentication_state.dart';
2222

23+
const _requestCodeCooldownDuration = Duration(seconds: 60);
24+
2325
/// {@template authentication_bloc}
2426
/// Bloc responsible for managing the authentication state of the application.
2527
/// {@endtemplate}
@@ -40,6 +42,7 @@ class AuthenticationBloc
4042
);
4143
on<AuthenticationVerifyCodeRequested>(_onAuthenticationVerifyCodeRequested);
4244
on<AuthenticationSignOutRequested>(_onAuthenticationSignOutRequested);
45+
on<AuthenticationCooldownCompleted>(_onAuthenticationCooldownCompleted);
4346
}
4447

4548
final AuthRepository _authenticationRepository;
@@ -72,18 +75,32 @@ class AuthenticationBloc
7275
AuthenticationRequestSignInCodeRequested event,
7376
Emitter<AuthenticationState> emit,
7477
) async {
78+
// Prevent request if already in cooldown
79+
if (state.cooldownEndTime != null &&
80+
state.cooldownEndTime!.isAfter(DateTime.now())) {
81+
return;
82+
}
83+
7584
emit(state.copyWith(status: AuthenticationStatus.requestCodeLoading));
7685
try {
7786
await _authenticationRepository.requestSignInCode(
7887
event.email,
7988
isDashboardLogin: true,
8089
);
90+
final cooldownEndTime = DateTime.now().add(_requestCodeCooldownDuration);
8191
emit(
8292
state.copyWith(
8393
status: AuthenticationStatus.codeSentSuccess,
8494
email: event.email,
95+
cooldownEndTime: cooldownEndTime,
8596
),
8697
);
98+
99+
// Start a timer to transition out of cooldown
100+
Timer(
101+
_requestCodeCooldownDuration,
102+
() => add(const AuthenticationCooldownCompleted()),
103+
);
87104
} on InvalidInputException catch (e) {
88105
emit(state.copyWith(status: AuthenticationStatus.failure, exception: e));
89106
} on UnauthorizedException catch (e) {
@@ -181,6 +198,18 @@ class AuthenticationBloc
181198
}
182199
}
183200

201+
void _onAuthenticationCooldownCompleted(
202+
AuthenticationCooldownCompleted event,
203+
Emitter<AuthenticationState> emit,
204+
) {
205+
emit(
206+
state.copyWith(
207+
status: AuthenticationStatus.initial,
208+
clearCooldownEndTime: true,
209+
),
210+
);
211+
}
212+
184213
@override
185214
Future<void> close() {
186215
_userAuthSubscription.cancel();

lib/authentication/bloc/authentication_event.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,11 @@ final class _AuthenticationStatusChanged extends AuthenticationEvent {
6868
@override
6969
List<Object?> get props => [user];
7070
}
71+
72+
/// {@template authentication_cooldown_completed}
73+
/// Event triggered when the sign-in code request cooldown has completed.
74+
/// {@endtemplate}
75+
final class AuthenticationCooldownCompleted extends AuthenticationEvent {
76+
/// {@macro authentication_cooldown_completed}
77+
const AuthenticationCooldownCompleted();
78+
}

lib/authentication/bloc/authentication_state.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class AuthenticationState extends Equatable {
3636
this.user,
3737
this.email,
3838
this.exception,
39+
this.cooldownEndTime,
3940
});
4041

4142
/// The current status of the authentication process.
@@ -50,8 +51,11 @@ final class AuthenticationState extends Equatable {
5051
/// The error describing an authentication failure, if any.
5152
final HttpException? exception;
5253

54+
/// The time when the cooldown for requesting a new code ends.
55+
final DateTime? cooldownEndTime;
56+
5357
@override
54-
List<Object?> get props => [status, user, email, exception];
58+
List<Object?> get props => [status, user, email, exception, cooldownEndTime];
5559

5660
/// Creates a copy of this [AuthenticationState] with the given fields
5761
/// replaced with the new values.
@@ -60,12 +64,16 @@ final class AuthenticationState extends Equatable {
6064
User? user,
6165
String? email,
6266
HttpException? exception,
67+
DateTime? cooldownEndTime,
68+
bool clearCooldownEndTime = false,
6369
}) {
6470
return AuthenticationState(
6571
status: status ?? this.status,
6672
user: user ?? this.user,
6773
email: email ?? this.email,
6874
exception: exception ?? this.exception,
75+
cooldownEndTime:
76+
clearCooldownEndTime ? null : cooldownEndTime ?? this.cooldownEndTime,
6977
);
7078
}
7179
}

lib/authentication/view/email_code_verification_page.dart

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'package:flutter/material.dart';
2-
import 'package:flutter/services.dart';
32
import 'package:flutter_bloc/flutter_bloc.dart';
43
import 'package:flutter_news_app_web_dashboard_full_source_code/app/bloc/app_bloc.dart';
54
import 'package:flutter_news_app_web_dashboard_full_source_code/app/config/config.dart';
65
import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/bloc/authentication_bloc.dart';
76
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
7+
import 'package:pinput/pinput.dart';
88
import 'package:ui_kit/ui_kit.dart';
99

1010
/// {@template email_code_verification_page}
@@ -131,10 +131,12 @@ class _EmailCodeVerificationFormState
131131
extends State<_EmailCodeVerificationForm> {
132132
final _formKey = GlobalKey<FormState>();
133133
final _codeController = TextEditingController();
134+
final _focusNode = FocusNode();
134135

135136
@override
136137
void dispose() {
137138
_codeController.dispose();
139+
_focusNode.dispose();
138140
super.dispose();
139141
}
140142

@@ -153,39 +155,48 @@ class _EmailCodeVerificationFormState
153155
Widget build(BuildContext context) {
154156
final l10n = AppLocalizationsX(context).l10n;
155157
final textTheme = Theme.of(context).textTheme;
158+
final colorScheme = Theme.of(context).colorScheme;
159+
160+
final defaultPinTheme = PinTheme(
161+
width: 56,
162+
height: 60,
163+
textStyle: textTheme.headlineSmall,
164+
decoration: BoxDecoration(
165+
color: colorScheme.surface,
166+
borderRadius: BorderRadius.circular(8),
167+
border: Border.all(color: colorScheme.onSurface.withOpacity(0.12)),
168+
),
169+
);
156170

157171
return Form(
158172
key: _formKey,
159173
child: Column(
160174
mainAxisSize: MainAxisSize.min,
161175
children: [
162-
Padding(
163-
// No horizontal padding needed if column is stretched
164-
// padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
165-
padding: EdgeInsets.zero,
166-
child: TextFormField(
167-
controller: _codeController,
168-
decoration: InputDecoration(
169-
labelText: l10n.emailCodeVerificationHint,
170-
// border: const OutlineInputBorder(),
171-
counterText: '',
176+
Pinput(
177+
length: 6,
178+
controller: _codeController,
179+
focusNode: _focusNode,
180+
defaultPinTheme: defaultPinTheme,
181+
onCompleted: (pin) => _submitForm(),
182+
validator: (value) {
183+
if (value == null || value.isEmpty) {
184+
return l10n.emailCodeValidationEmptyError;
185+
}
186+
if (value.length != 6) {
187+
return l10n.emailCodeValidationLengthError;
188+
}
189+
return null;
190+
},
191+
focusedPinTheme: defaultPinTheme.copyWith(
192+
decoration: defaultPinTheme.decoration!.copyWith(
193+
border: Border.all(color: colorScheme.primary),
194+
),
195+
),
196+
errorPinTheme: defaultPinTheme.copyWith(
197+
decoration: defaultPinTheme.decoration!.copyWith(
198+
border: Border.all(color: colorScheme.error),
172199
),
173-
keyboardType: TextInputType.number,
174-
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
175-
maxLength: 6,
176-
textAlign: TextAlign.center,
177-
style: textTheme.headlineSmall,
178-
enabled: !widget.isLoading,
179-
validator: (value) {
180-
if (value == null || value.isEmpty) {
181-
return l10n.emailCodeValidationEmptyError;
182-
}
183-
if (value.length != 6) {
184-
return l10n.emailCodeValidationLengthError;
185-
}
186-
return null;
187-
},
188-
onFieldSubmitted: widget.isLoading ? null : (_) => _submitForm(),
189200
),
190201
),
191202
const SizedBox(height: AppSpacing.xxl),

0 commit comments

Comments
 (0)