Skip to content

Commit 18bb5bf

Browse files
committed
feat(authentication): implement cooldown timer for sign-in code requests
- Add cooldown functionality to prevent frequent sign-in code requests - Implement Timer to handle countdown and update UI accordingly - Update AuthenticationBloc to manage cooldown end time - Modify UI to show cooldown remaining time and disable button when necessary
1 parent c435c82 commit 18bb5bf

File tree

1 file changed

+103
-45
lines changed

1 file changed

+103
-45
lines changed

lib/authentication/view/request_code_page.dart

Lines changed: 103 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//
22
// ignore_for_file: lines_longer_than_80_chars
33

4+
import 'dart:async';
5+
46
import 'package:flutter/material.dart';
57
import 'package:flutter_bloc/flutter_bloc.dart';
68
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart';
@@ -161,20 +163,58 @@ class _EmailLinkForm extends StatefulWidget {
161163
class _EmailLinkFormState extends State<_EmailLinkForm> {
162164
final _emailController = TextEditingController();
163165
final _formKey = GlobalKey<FormState>();
166+
Timer? _cooldownTimer;
167+
int _cooldownSeconds = 0;
168+
169+
@override
170+
void initState() {
171+
super.initState();
172+
final authState = context.read<AuthenticationBloc>().state;
173+
if (authState.cooldownEndTime != null &&
174+
authState.cooldownEndTime!.isAfter(DateTime.now())) {
175+
_startCooldownTimer(authState.cooldownEndTime!);
176+
}
177+
}
164178

165179
@override
166180
void dispose() {
167181
_emailController.dispose();
182+
_cooldownTimer?.cancel();
168183
super.dispose();
169184
}
170185

186+
void _startCooldownTimer(DateTime endTime) {
187+
final now = DateTime.now();
188+
if (now.isBefore(endTime)) {
189+
setState(() {
190+
_cooldownSeconds = endTime.difference(now).inSeconds;
191+
});
192+
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
193+
final remaining = endTime.difference(DateTime.now()).inSeconds;
194+
if (remaining > 0) {
195+
setState(() {
196+
_cooldownSeconds = remaining;
197+
});
198+
} else {
199+
timer.cancel();
200+
setState(() {
201+
_cooldownSeconds = 0;
202+
});
203+
context
204+
.read<AuthenticationBloc>()
205+
.add(const AuthenticationCooldownCompleted());
206+
}
207+
});
208+
}
209+
}
210+
171211
void _submitForm() {
172212
if (_formKey.currentState!.validate()) {
173213
context.read<AuthenticationBloc>().add(
174-
AuthenticationRequestSignInCodeRequested(
175-
email: _emailController.text.trim(),
176-
),
177-
);
214+
AuthenticationRequestSignInCodeRequested(
215+
email: _emailController.text.trim(),
216+
),
217+
);
178218
}
179219
}
180220

@@ -184,49 +224,67 @@ class _EmailLinkFormState extends State<_EmailLinkForm> {
184224
final textTheme = Theme.of(context).textTheme;
185225
final colorScheme = Theme.of(context).colorScheme;
186226

187-
return Form(
188-
key: _formKey,
189-
child: Column(
190-
crossAxisAlignment: CrossAxisAlignment.stretch,
191-
children: [
192-
TextFormField(
193-
controller: _emailController,
194-
decoration: InputDecoration(
195-
labelText: l10n.requestCodeEmailLabel,
196-
hintText: l10n.requestCodeEmailHint,
197-
// border: const OutlineInputBorder(),
227+
return BlocListener<AuthenticationBloc, AuthenticationState>(
228+
listenWhen: (previous, current) =>
229+
previous.cooldownEndTime != current.cooldownEndTime,
230+
listener: (context, state) {
231+
if (state.cooldownEndTime != null &&
232+
state.cooldownEndTime!.isAfter(DateTime.now())) {
233+
_cooldownTimer?.cancel();
234+
_startCooldownTimer(state.cooldownEndTime!);
235+
}
236+
},
237+
child: Form(
238+
key: _formKey,
239+
child: Column(
240+
crossAxisAlignment: CrossAxisAlignment.stretch,
241+
children: [
242+
TextFormField(
243+
controller: _emailController,
244+
decoration: InputDecoration(
245+
labelText: l10n.requestCodeEmailLabel,
246+
hintText: l10n.requestCodeEmailHint,
247+
),
248+
keyboardType: TextInputType.emailAddress,
249+
autocorrect: false,
250+
textInputAction: TextInputAction.done,
251+
enabled: !widget.isLoading && _cooldownSeconds == 0,
252+
validator: (value) {
253+
if (value == null || value.isEmpty || !value.contains('@')) {
254+
return l10n.accountLinkingEmailValidationError;
255+
}
256+
return null;
257+
},
258+
onFieldSubmitted:
259+
widget.isLoading || _cooldownSeconds > 0 ? null : (_) => _submitForm(),
198260
),
199-
keyboardType: TextInputType.emailAddress,
200-
autocorrect: false,
201-
textInputAction: TextInputAction.done,
202-
enabled: !widget.isLoading,
203-
validator: (value) {
204-
if (value == null || value.isEmpty || !value.contains('@')) {
205-
return l10n.accountLinkingEmailValidationError;
206-
}
207-
return null;
208-
},
209-
onFieldSubmitted: (_) => _submitForm(),
210-
),
211-
const SizedBox(height: AppSpacing.lg),
212-
ElevatedButton(
213-
onPressed: widget.isLoading ? null : _submitForm,
214-
style: ElevatedButton.styleFrom(
215-
padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
216-
textStyle: textTheme.labelLarge,
261+
const SizedBox(height: AppSpacing.lg),
262+
ElevatedButton(
263+
onPressed:
264+
widget.isLoading || _cooldownSeconds > 0 ? null : _submitForm,
265+
style: ElevatedButton.styleFrom(
266+
padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
267+
textStyle: textTheme.labelLarge,
268+
),
269+
child: widget.isLoading
270+
? SizedBox(
271+
height: AppSpacing.xl,
272+
width: AppSpacing.xl,
273+
child: CircularProgressIndicator(
274+
strokeWidth: 2,
275+
color: colorScheme.onPrimary,
276+
),
277+
)
278+
: _cooldownSeconds > 0
279+
? Text(
280+
l10n.requestCodeResendButtonCooldown(
281+
_cooldownSeconds,
282+
),
283+
)
284+
: Text(l10n.requestCodeSendCodeButton),
217285
),
218-
child: widget.isLoading
219-
? SizedBox(
220-
height: AppSpacing.xl,
221-
width: AppSpacing.xl,
222-
child: CircularProgressIndicator(
223-
strokeWidth: 2,
224-
color: colorScheme.onPrimary,
225-
),
226-
)
227-
: Text(l10n.requestCodeSendCodeButton),
228-
),
229-
],
286+
],
287+
),
230288
),
231289
);
232290
}

0 commit comments

Comments
 (0)