From f5539cdf4e8e962ffff75c4b5a76fd569e42b58c Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:42:10 +0100 Subject: [PATCH 01/15] feat: add pinput dependency and update pubspec.lock - Add pinput package to pubspec.yaml for improved text input handling - Update pubspec.lock with new dependencies: - pinput: ^5.0.1 - universal_platform: ^1.1.0 (transitive dependency) - Ensure proper formatting and version management for all packages --- pubspec.lock | 16 ++++++++++++++++ pubspec.yaml | 1 + 2 files changed, 17 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 64fc3a68..01f37a5e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -428,6 +428,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" + url: "https://pub.dev" + source: hosted + version: "5.0.1" platform: dependency: transitive description: @@ -578,6 +586,14 @@ packages: url: "https://github.com/flutter-news-app-full-source-code/ui-kit.git" source: git version: "0.0.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" uuid: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e1bccbe2..173d0628 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: git: url: https://github.com/flutter-news-app-full-source-code/kv-storage-shared-preferences.git logging: ^1.3.0 + pinput: ^5.0.1 timeago: ^3.7.1 ui_kit: git: From 752d368ba2debe6c1463d025b92ec92ae687034b Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:42:26 +0100 Subject: [PATCH 02/15] build: change app environment from demo to development - Modified the appEnvironment constant in main.dart - Changed value from AppEnvironment.demo to AppEnvironment.development --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 340698d1..d6ca6946 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app/config/confi import 'package:flutter_news_app_web_dashboard_full_source_code/bootstrap.dart'; // Define the current application environment (production/development/demo). -const AppEnvironment appEnvironment = AppEnvironment.demo; +const AppEnvironment appEnvironment = AppEnvironment.development; @JS('removeSplashFromWeb') external void removeSplashFromWeb(); From af996cc8e3417438d7eb108c71c49f1b1850fb63 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:44:20 +0100 Subject: [PATCH 03/15] feat(authentication): add requestCodeCooldown status and cooldownEndTime prop - Add requestCodeCooldown status to AuthenticationStatus enum - Add cooldownEndTime property to AuthenticationState - Update props list in AuthenticationState to include cooldownEndTime - Modify copyWith method to include cooldownEndTime parameter --- lib/authentication/bloc/authentication_state.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/authentication/bloc/authentication_state.dart b/lib/authentication/bloc/authentication_state.dart index 14449f11..1a6043d8 100644 --- a/lib/authentication/bloc/authentication_state.dart +++ b/lib/authentication/bloc/authentication_state.dart @@ -22,6 +22,9 @@ enum AuthenticationStatus { /// The sign-in code was sent successfully. codeSentSuccess, + /// The user is in a cooldown period after requesting a code. + requestCodeCooldown, + /// An authentication operation failed. failure, } @@ -36,6 +39,7 @@ final class AuthenticationState extends Equatable { this.user, this.email, this.exception, + this.cooldownEndTime, }); /// The current status of the authentication process. @@ -50,8 +54,11 @@ final class AuthenticationState extends Equatable { /// The error describing an authentication failure, if any. final HttpException? exception; + /// The time when the cooldown for requesting a new code ends. + final DateTime? cooldownEndTime; + @override - List get props => [status, user, email, exception]; + List get props => [status, user, email, exception, cooldownEndTime]; /// Creates a copy of this [AuthenticationState] with the given fields /// replaced with the new values. @@ -60,12 +67,14 @@ final class AuthenticationState extends Equatable { User? user, String? email, HttpException? exception, + DateTime? cooldownEndTime, }) { return AuthenticationState( status: status ?? this.status, user: user ?? this.user, email: email ?? this.email, exception: exception ?? this.exception, + cooldownEndTime: cooldownEndTime ?? this.cooldownEndTime, ); } } From 098c5a2154f7fa92110f4ec37d84704218599e55 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:44:46 +0100 Subject: [PATCH 04/15] feat(authentication): add AuthenticationCooldownCompleted event Add a new event to the authentication bloc to handle the completion of the sign-in code request cooldown. --- lib/authentication/bloc/authentication_event.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/authentication/bloc/authentication_event.dart b/lib/authentication/bloc/authentication_event.dart index c6001110..b356c42c 100644 --- a/lib/authentication/bloc/authentication_event.dart +++ b/lib/authentication/bloc/authentication_event.dart @@ -68,3 +68,11 @@ final class _AuthenticationStatusChanged extends AuthenticationEvent { @override List get props => [user]; } + +/// {@template authentication_cooldown_completed} +/// Event triggered when the sign-in code request cooldown has completed. +/// {@endtemplate} +final class AuthenticationCooldownCompleted extends AuthenticationEvent { + /// {@macro authentication_cooldown_completed} + const AuthenticationCooldownCompleted(); +} From ead64014d5663e643d87718bab7e97ae2a5a8780 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:45:10 +0100 Subject: [PATCH 05/15] feat(authentication): implement cooldown for sign-in code requests - Add cooldown functionality to prevent frequent code requests - Introduce new AuthenticationStatus.state: requestCodeCooldown - Implement timer to transition out of cooldown after 60 seconds - Update UI feedback during cooldown period --- .../bloc/authentication_bloc.dart | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index aeb92bcd..97c7af98 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -20,6 +20,8 @@ import 'package:equatable/equatable.dart'; part 'authentication_event.dart'; part 'authentication_state.dart'; +const _requestCodeCooldownDuration = Duration(seconds: 60); + /// {@template authentication_bloc} /// Bloc responsible for managing the authentication state of the application. /// {@endtemplate} @@ -40,6 +42,7 @@ class AuthenticationBloc ); on(_onAuthenticationVerifyCodeRequested); on(_onAuthenticationSignOutRequested); + on(_onAuthenticationCooldownCompleted); } final AuthRepository _authenticationRepository; @@ -72,18 +75,32 @@ class AuthenticationBloc AuthenticationRequestSignInCodeRequested event, Emitter emit, ) async { + // Prevent request if already in cooldown + if (state.status == AuthenticationStatus.requestCodeCooldown) return; + emit(state.copyWith(status: AuthenticationStatus.requestCodeLoading)); try { await _authenticationRepository.requestSignInCode( event.email, isDashboardLogin: true, ); + final cooldownEndTime = DateTime.now().add(_requestCodeCooldownDuration); emit( state.copyWith( status: AuthenticationStatus.codeSentSuccess, email: event.email, + cooldownEndTime: cooldownEndTime, ), ); + // Transition to cooldown state after a brief moment + await Future.delayed(const Duration(milliseconds: 100)); + emit(state.copyWith(status: AuthenticationStatus.requestCodeCooldown)); + + // Start a timer to transition out of cooldown + Timer( + _requestCodeCooldownDuration, + () => add(const AuthenticationCooldownCompleted()), + ); } on InvalidInputException catch (e) { emit(state.copyWith(status: AuthenticationStatus.failure, exception: e)); } on UnauthorizedException catch (e) { @@ -181,6 +198,15 @@ class AuthenticationBloc } } + void _onAuthenticationCooldownCompleted( + AuthenticationCooldownCompleted event, + Emitter emit, + ) { + if (state.status == AuthenticationStatus.requestCodeCooldown) { + emit(state.copyWith(status: AuthenticationStatus.initial)); + } + } + @override Future close() { _userAuthSubscription.cancel(); From 35e3e28b9deafc05fc3c033f74a30f32e4c2feaf Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:45:47 +0100 Subject: [PATCH 06/15] feat(authentication): add cooldown timer for sign-in code requests - Implement cooldown timer functionality in EmailLinkForm - Add state management for cooldown seconds and timer - Update UI to reflect cooldown state - Integrate cooldown logic with AuthenticationBloc --- .../view/request_code_page.dart | 145 ++++++++++++------ 1 file changed, 100 insertions(+), 45 deletions(-) diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index 84ed468a..748b3384 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -183,20 +183,59 @@ class _EmailLinkForm extends StatefulWidget { class _EmailLinkFormState extends State<_EmailLinkForm> { final _emailController = TextEditingController(); final _formKey = GlobalKey(); + Timer? _cooldownTimer; + int _cooldownSeconds = 0; + + @override + void initState() { + super.initState(); + final authState = context.read().state; + if (authState.status == AuthenticationStatus.requestCodeCooldown && + authState.cooldownEndTime != null) { + _startCooldownTimer(authState.cooldownEndTime!); + } + } @override void dispose() { _emailController.dispose(); + _cooldownTimer?.cancel(); super.dispose(); } + void _startCooldownTimer(DateTime endTime) { + final now = DateTime.now(); + if (now.isBefore(endTime)) { + setState(() { + _cooldownSeconds = endTime.difference(now).inSeconds; + }); + _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final remaining = endTime.difference(DateTime.now()).inSeconds; + if (remaining > 0) { + setState(() { + _cooldownSeconds = remaining; + }); + } else { + timer.cancel(); + setState(() { + _cooldownSeconds = 0; + }); + // Optionally, trigger an event to reset the bloc state if needed + context + .read() + .add(const AuthenticationCooldownCompleted()); + } + }); + } + } + void _submitForm() { if (_formKey.currentState!.validate()) { context.read().add( - AuthenticationRequestSignInCodeRequested( - email: _emailController.text.trim(), - ), - ); + AuthenticationRequestSignInCodeRequested( + email: _emailController.text.trim(), + ), + ); } } @@ -206,49 +245,65 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; - return Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: _emailController, - decoration: InputDecoration( - labelText: l10n.requestCodeEmailLabel, - hintText: l10n.requestCodeEmailHint, - // border: const OutlineInputBorder(), + return BlocListener( + listener: (context, state) { + if (state.status == AuthenticationStatus.requestCodeCooldown && + state.cooldownEndTime != null) { + _cooldownTimer?.cancel(); + _startCooldownTimer(state.cooldownEndTime!); + } + }, + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: _emailController, + decoration: InputDecoration( + labelText: l10n.requestCodeEmailLabel, + hintText: l10n.requestCodeEmailHint, + ), + keyboardType: TextInputType.emailAddress, + autocorrect: false, + textInputAction: TextInputAction.done, + enabled: !widget.isLoading && _cooldownSeconds == 0, + validator: (value) { + if (value == null || value.isEmpty || !value.contains('@')) { + return l10n.accountLinkingEmailValidationError; + } + return null; + }, + onFieldSubmitted: + widget.isLoading || _cooldownSeconds > 0 ? null : (_) => _submitForm(), ), - keyboardType: TextInputType.emailAddress, - autocorrect: false, - textInputAction: TextInputAction.done, - enabled: !widget.isLoading, - validator: (value) { - if (value == null || value.isEmpty || !value.contains('@')) { - return l10n.accountLinkingEmailValidationError; - } - return null; - }, - onFieldSubmitted: (_) => _submitForm(), - ), - const SizedBox(height: AppSpacing.lg), - ElevatedButton( - onPressed: widget.isLoading ? null : _submitForm, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), - textStyle: textTheme.labelLarge, + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: + widget.isLoading || _cooldownSeconds > 0 ? null : _submitForm, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), + textStyle: textTheme.labelLarge, + ), + child: widget.isLoading + ? SizedBox( + height: AppSpacing.xl, + width: AppSpacing.xl, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colorScheme.onPrimary, + ), + ) + : _cooldownSeconds > 0 + ? Text( + l10n.requestCodeResendButtonCooldown( + _cooldownSeconds, + ), + ) + : Text(l10n.requestCodeSendCodeButton), ), - child: widget.isLoading - ? SizedBox( - height: AppSpacing.xl, - width: AppSpacing.xl, - child: CircularProgressIndicator( - strokeWidth: 2, - color: colorScheme.onPrimary, - ), - ) - : Text(l10n.requestCodeSendCodeButton), - ), - ], + ], + ), ), ); } From 83e68f0146cbcc4cab0fca2c1d0ffb89981b028d Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:51:14 +0100 Subject: [PATCH 07/15] refactor(authentication): add dart:async import for future use - Import dart:async library in request_code_page.dart - Prepare for potential future implementations that may require Timer or Future functionalities --- lib/authentication/view/request_code_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index 748b3384..2ff7a147 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -1,6 +1,8 @@ // // ignore_for_file: lines_longer_than_80_chars +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app/bloc/app_bloc.dart'; From 9c41e599b82a95c0e5c8cc0fa2811cdb7c802c47 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 17:51:41 +0100 Subject: [PATCH 08/15] feat(localization): add resend verification code button label - Add new localization strings for both Arabic and English - Implement placeholder for seconds in cooldown period - Update existing localization files for both languages --- lib/l10n/app_localizations.dart | 6 ++++++ lib/l10n/app_localizations_ar.dart | 5 +++++ lib/l10n/app_localizations_en.dart | 5 +++++ lib/l10n/arb/app_ar.arb | 11 ++++++++++- lib/l10n/arb/app_en.arb | 9 +++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 92e7518a..70f412da 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -158,6 +158,12 @@ abstract class AppLocalizations { /// **'Send Code'** String get requestCodeSendCodeButton; + /// Button label for resending the verification code after a cooldown + /// + /// In en, this message translates to: + /// **'Resend in {seconds}s'** + String requestCodeResendButtonCooldown(int seconds); + /// Title for the email code verification page /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index e20f55a8..3c56a72e 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -42,6 +42,11 @@ class AppLocalizationsAr extends AppLocalizations { @override String get requestCodeSendCodeButton => 'إرسال الرمز'; + @override + String requestCodeResendButtonCooldown(int seconds) { + return 'إعادة الإرسال في $seconds ثانية'; + } + @override String get emailCodeSentPageTitle => 'التحقق من الرمز'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bf977e7f..cec717e3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -41,6 +41,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get requestCodeSendCodeButton => 'Send Code'; + @override + String requestCodeResendButtonCooldown(int seconds) { + return 'Resend in ${seconds}s'; + } + @override String get emailCodeSentPageTitle => 'Verify Code'; diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 3a43f8a3..39b5d6a7 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -39,6 +39,15 @@ "@requestCodeSendCodeButton": { "description": "تسمية زر إرسال رمز التحقق" }, + "requestCodeResendButtonCooldown": "إعادة الإرسال في {seconds} ثانية", + "@requestCodeResendButtonCooldown": { + "description": "تسمية زر إعادة إرسال رمز التحقق بعد فترة تهدئة", + "placeholders": { + "seconds": { + "type": "int" + } + } + }, "emailCodeSentPageTitle": "التحقق من الرمز", "@emailCodeSentPageTitle": { "description": "عنوان صفحة التحقق من رمز البريد الإلكتروني" @@ -1067,4 +1076,4 @@ "@feedActionTypeEnableNotifications": { "description": "نوع إجراء الموجز لتفعيل الإشعارات" } -} +} \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bbe62905..dedf0be4 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -39,6 +39,15 @@ "@requestCodeSendCodeButton": { "description": "Button label for sending the verification code" }, + "requestCodeResendButtonCooldown": "Resend in {seconds}s", + "@requestCodeResendButtonCooldown": { + "description": "Button label for resending the verification code after a cooldown", + "placeholders": { + "seconds": { + "type": "int" + } + } + }, "emailCodeSentPageTitle": "Verify Code", "@emailCodeSentPageTitle": { "description": "Title for the email code verification page" From 9536167d94b1319c5c20e10718393391eb0494ad Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 18:05:47 +0100 Subject: [PATCH 09/15] feat(authentication): replace TextFormField with Pinput for email code verification - Add pinput package for improved pin input experience - Implement Pinput with custom PinTheme - Update form validation and submission logic - Adjust UI to match new input style --- .../view/email_code_verification_page.dart | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/lib/authentication/view/email_code_verification_page.dart b/lib/authentication/view/email_code_verification_page.dart index 1dcf3c2a..75dbb46f 100644 --- a/lib/authentication/view/email_code_verification_page.dart +++ b/lib/authentication/view/email_code_verification_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app/bloc/app_blo import 'package:flutter_news_app_web_dashboard_full_source_code/app/config/config.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/bloc/authentication_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:pinput/pinput.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template email_code_verification_page} @@ -131,10 +132,12 @@ class _EmailCodeVerificationFormState extends State<_EmailCodeVerificationForm> { final _formKey = GlobalKey(); final _codeController = TextEditingController(); + final _focusNode = FocusNode(); @override void dispose() { _codeController.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -153,39 +156,48 @@ class _EmailCodeVerificationFormState Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + + final defaultPinTheme = PinTheme( + width: 56, + height: 60, + textStyle: textTheme.headlineSmall, + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colorScheme.onSurface.withOpacity(0.12)), + ), + ); return Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - // No horizontal padding needed if column is stretched - // padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - padding: EdgeInsets.zero, - child: TextFormField( - controller: _codeController, - decoration: InputDecoration( - labelText: l10n.emailCodeVerificationHint, - // border: const OutlineInputBorder(), - counterText: '', + Pinput( + length: 6, + controller: _codeController, + focusNode: _focusNode, + defaultPinTheme: defaultPinTheme, + onCompleted: (pin) => _submitForm(), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.emailCodeValidationEmptyError; + } + if (value.length != 6) { + return l10n.emailCodeValidationLengthError; + } + return null; + }, + focusedPinTheme: defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration!.copyWith( + border: Border.all(color: colorScheme.primary), + ), + ), + errorPinTheme: defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration!.copyWith( + border: Border.all(color: colorScheme.error), ), - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - maxLength: 6, - textAlign: TextAlign.center, - style: textTheme.headlineSmall, - enabled: !widget.isLoading, - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.emailCodeValidationEmptyError; - } - if (value.length != 6) { - return l10n.emailCodeValidationLengthError; - } - return null; - }, - onFieldSubmitted: widget.isLoading ? null : (_) => _submitForm(), ), ), const SizedBox(height: AppSpacing.xxl), From eb20029d863aa09f709cc5d0c97459b25cd56dc1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 18:07:31 +0100 Subject: [PATCH 10/15] refactor(authentication): remove unused import - Remove unused import statement for 'package:flutter/services.dart' - This change simplifies the code and reduces potential confusion for developers --- lib/authentication/view/email_code_verification_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/authentication/view/email_code_verification_page.dart b/lib/authentication/view/email_code_verification_page.dart index 75dbb46f..c8348ac6 100644 --- a/lib/authentication/view/email_code_verification_page.dart +++ b/lib/authentication/view/email_code_verification_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app/bloc/app_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app/config/config.dart'; From 4c98e38bcc06aec825e79b959bec752c6475db2c Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 18:07:42 +0100 Subject: [PATCH 11/15] fix(l10n): add locale information to ARB files - Add @@locale metadata to app_ar.arb and app_en.arb files - This information is used by Flutter to identify the locale of each ARB file --- lib/l10n/arb/app_ar.arb | 1 + lib/l10n/arb/app_en.arb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 39b5d6a7..d4e04827 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1,4 +1,5 @@ { + "@@locale": "ar", "authenticationPageHeadline": "الوصول إلى لوحة التحكم", "@authenticationPageHeadline": { "description": "عنوان صفحة المصادقة الرئيسية" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index dedf0be4..7ce83bb5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,4 +1,5 @@ { + "@@locale": "en", "authenticationPageHeadline": "Dashboard Access", "@authenticationPageHeadline": { "description": "Headline for the main authentication page" From d912f48086dcb6f2935c6a76ffbb0e261da91731 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 18:27:07 +0100 Subject: [PATCH 12/15] feat(authentication): add option to clear cooldown end time - Add clearCooldownEndTime parameter to copyWith method - Set cooldownEndTime to null when clearCooldownEndTime is true --- lib/authentication/bloc/authentication_state.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/authentication/bloc/authentication_state.dart b/lib/authentication/bloc/authentication_state.dart index 1a6043d8..cdbdf929 100644 --- a/lib/authentication/bloc/authentication_state.dart +++ b/lib/authentication/bloc/authentication_state.dart @@ -68,13 +68,15 @@ final class AuthenticationState extends Equatable { String? email, HttpException? exception, DateTime? cooldownEndTime, + bool clearCooldownEndTime = false, }) { return AuthenticationState( status: status ?? this.status, user: user ?? this.user, email: email ?? this.email, exception: exception ?? this.exception, - cooldownEndTime: cooldownEndTime ?? this.cooldownEndTime, + cooldownEndTime: + clearCooldownEndTime ? null : cooldownEndTime ?? this.cooldownEndTime, ); } } From cc5f197ee939afc78e063e98a83af14f11467208 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 18:27:32 +0100 Subject: [PATCH 13/15] fix(authentication): add flag to clear cooldown end time - Update AuthenticationBloc to include clearCooldownEndTime flag when resetting status to initial - This change ensures proper reset of cooldown state and prevents potential issues with leftover values --- lib/authentication/bloc/authentication_bloc.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index 97c7af98..d402ed5d 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -203,7 +203,12 @@ class AuthenticationBloc Emitter emit, ) { if (state.status == AuthenticationStatus.requestCodeCooldown) { - emit(state.copyWith(status: AuthenticationStatus.initial)); + emit( + state.copyWith( + status: AuthenticationStatus.initial, + clearCooldownEndTime: true, + ), + ); } } From fd82b19077dc0df3e59450e6b10f82c3bc44154e Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 18:28:28 +0100 Subject: [PATCH 14/15] fix(authentication): improve cooldown timer check - Update cooldown timer condition to ensure it only starts when cooldownEndTime is in the future - Remove redundant status check for better readability --- lib/authentication/view/request_code_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index 2ff7a147..9823fa1c 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -192,8 +192,8 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { void initState() { super.initState(); final authState = context.read().state; - if (authState.status == AuthenticationStatus.requestCodeCooldown && - authState.cooldownEndTime != null) { + if (authState.cooldownEndTime != null && + authState.cooldownEndTime!.isAfter(DateTime.now())) { _startCooldownTimer(authState.cooldownEndTime!); } } From 66b87979da5b1818c643a0574fa26f69c558e6a1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 28 Jul 2025 18:38:44 +0100 Subject: [PATCH 15/15] refactor(auth): remove requestCodeCooldown status Refactored the authentication BLoC and UI to rely solely on the `cooldownEndTime` timestamp for managing the sign-in code request cooldown. This removes the redundant `requestCodeCooldown` from the `AuthenticationStatus` enum, simplifying the state machine and establishing a single source of truth for the cooldown logic. This change fixes the bug where the cooldown timer would not display correctly after a failed verification and makes the implementation more robust and maintainable. --- .../bloc/authentication_bloc.dart | 22 +++++++++---------- .../bloc/authentication_state.dart | 3 --- .../view/request_code_page.dart | 6 +++-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index d402ed5d..62e197eb 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -76,7 +76,10 @@ class AuthenticationBloc Emitter emit, ) async { // Prevent request if already in cooldown - if (state.status == AuthenticationStatus.requestCodeCooldown) return; + if (state.cooldownEndTime != null && + state.cooldownEndTime!.isAfter(DateTime.now())) { + return; + } emit(state.copyWith(status: AuthenticationStatus.requestCodeLoading)); try { @@ -92,9 +95,6 @@ class AuthenticationBloc cooldownEndTime: cooldownEndTime, ), ); - // Transition to cooldown state after a brief moment - await Future.delayed(const Duration(milliseconds: 100)); - emit(state.copyWith(status: AuthenticationStatus.requestCodeCooldown)); // Start a timer to transition out of cooldown Timer( @@ -202,14 +202,12 @@ class AuthenticationBloc AuthenticationCooldownCompleted event, Emitter emit, ) { - if (state.status == AuthenticationStatus.requestCodeCooldown) { - emit( - state.copyWith( - status: AuthenticationStatus.initial, - clearCooldownEndTime: true, - ), - ); - } + emit( + state.copyWith( + status: AuthenticationStatus.initial, + clearCooldownEndTime: true, + ), + ); } @override diff --git a/lib/authentication/bloc/authentication_state.dart b/lib/authentication/bloc/authentication_state.dart index cdbdf929..cdcfe091 100644 --- a/lib/authentication/bloc/authentication_state.dart +++ b/lib/authentication/bloc/authentication_state.dart @@ -22,9 +22,6 @@ enum AuthenticationStatus { /// The sign-in code was sent successfully. codeSentSuccess, - /// The user is in a cooldown period after requesting a code. - requestCodeCooldown, - /// An authentication operation failed. failure, } diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index 9823fa1c..f5bf9f65 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -248,9 +248,11 @@ class _EmailLinkFormState extends State<_EmailLinkForm> { final colorScheme = Theme.of(context).colorScheme; return BlocListener( + listenWhen: (previous, current) => + previous.cooldownEndTime != current.cooldownEndTime, listener: (context, state) { - if (state.status == AuthenticationStatus.requestCodeCooldown && - state.cooldownEndTime != null) { + if (state.cooldownEndTime != null && + state.cooldownEndTime!.isAfter(DateTime.now())) { _cooldownTimer?.cancel(); _startCooldownTimer(state.cooldownEndTime!); }