diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 3d56fd3..03ce20f 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -212,6 +212,8 @@ class ApiClient { ApiConstants.authForgotPassword, ApiConstants.authResetPassword, ApiConstants.authVerifyEmail, + ApiConstants.twoFactorVerify, + ApiConstants.twoFactorVerifyBackup, ]; return authPaths.any((path) => url.contains(path)); } diff --git a/lib/features/agents/presentation/screens/agent_home_screen.dart b/lib/features/agents/presentation/screens/agent_home_screen.dart index 663d41c..3192549 100644 --- a/lib/features/agents/presentation/screens/agent_home_screen.dart +++ b/lib/features/agents/presentation/screens/agent_home_screen.dart @@ -89,6 +89,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { bool _isTogglingAvailability = false; bool _showEmail = false; bool _showPhone = false; + final Set _expandedTagSections = {}; File? _localAvatarFile; Future _pickAndUploadAvatar() async { @@ -729,7 +730,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { color: AppColors.primaryDark)), ), const SizedBox(height: 8), - _buildTagsWrap(state.licensingAreas), + _buildTagsWrap(state.licensingAreas, sectionKey: 'licensing'), const SizedBox(height: 16), ], if (state.expertiseYears.isNotEmpty) ...[ @@ -747,7 +748,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { .map((e) => e['years']!.isNotEmpty ? '${e['area']} – ${e['years']}' : e['area']!) - .toList()), + .toList(), sectionKey: 'expertise'), ], if (state.certifications.isNotEmpty) ...[ const SizedBox(height: 16), @@ -833,8 +834,9 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { ); } - Widget _buildTagsWrap(List tags) { - final displayTags = tags.take(6).toList(); + Widget _buildTagsWrap(List tags, {required String sectionKey}) { + final isExpanded = _expandedTagSections.contains(sectionKey); + final displayTags = isExpanded ? tags : tags.take(6).toList(); final remaining = tags.length - 6; return Wrap( @@ -857,19 +859,32 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { color: AppColors.primaryDark)), )), if (remaining > 0) - Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all(color: AppColors.primaryDark, width: 0.1), + GestureDetector( + onTap: () { + setState(() { + if (isExpanded) { + _expandedTagSections.remove(sectionKey); + } else { + _expandedTagSections.add(sectionKey); + } + }); + }, + behavior: HitTestBehavior.opaque, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.primaryDark, width: 0.1), + ), + child: Text( + isExpanded ? 'Show Less' : '+$remaining More', + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 15, + fontWeight: FontWeight.w300, + color: AppColors.primaryDark)), ), - child: Text('+$remaining More', - style: const TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 15, - fontWeight: FontWeight.w300, - color: AppColors.primaryDark)), ), ], ); diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart index 0bea744..c9c4003 100644 --- a/lib/features/auth/presentation/providers/auth_provider.dart +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -216,7 +216,12 @@ class AuthNotifier extends StateNotifier { Future verifyTwoFactor(String token) async { final tempToken = state.tempToken; - if (tempToken == null) return; + if (tempToken == null) { + state = state.copyWith( + errorMessage: 'Session expired. Please login again.', + ); + return; + } state = state.copyWith( status: AuthStatus.loading, @@ -235,13 +240,16 @@ class AuthNotifier extends StateNotifier { _connectSocket(); _initPushNotifications(); } on ApiException catch (e) { + // Preserve tempToken and requiresTwoFactor so user can retry state = state.copyWith( status: AuthStatus.initial, + requiresTwoFactor: true, errorMessage: e.message, ); } catch (e) { state = state.copyWith( status: AuthStatus.initial, + requiresTwoFactor: true, errorMessage: 'Verification failed. Please try again.', ); } @@ -249,7 +257,12 @@ class AuthNotifier extends StateNotifier { Future verifyBackupCode(String backupCode) async { final tempToken = state.tempToken; - if (tempToken == null) return; + if (tempToken == null) { + state = state.copyWith( + errorMessage: 'Session expired. Please login again.', + ); + return; + } state = state.copyWith( status: AuthStatus.loading, @@ -270,11 +283,13 @@ class AuthNotifier extends StateNotifier { } on ApiException catch (e) { state = state.copyWith( status: AuthStatus.initial, + requiresTwoFactor: true, errorMessage: e.message, ); } catch (e) { state = state.copyWith( status: AuthStatus.initial, + requiresTwoFactor: true, errorMessage: 'Verification failed. Please try again.', ); } diff --git a/lib/features/auth/presentation/screens/verify_2fa_screen.dart b/lib/features/auth/presentation/screens/verify_2fa_screen.dart index 3ad548b..8c19c9b 100644 --- a/lib/features/auth/presentation/screens/verify_2fa_screen.dart +++ b/lib/features/auth/presentation/screens/verify_2fa_screen.dart @@ -18,6 +18,7 @@ class _Verify2faScreenState extends ConsumerState { final List _focusNodes = List.generate(6, (_) => FocusNode()); final TextEditingController _backupController = TextEditingController(); bool _useBackupCode = false; + bool _isSubmitting = false; @override void dispose() { @@ -69,13 +70,17 @@ class _Verify2faScreenState extends ConsumerState { } void _submit() { + if (_isSubmitting) return; + if (_useBackupCode) { final code = _backupController.text.trim(); if (code.isEmpty) return; + _isSubmitting = true; ref.read(authProvider.notifier).verifyBackupCode(code); } else { final code = _otpCode; if (code.length != 6) return; + _isSubmitting = true; ref.read(authProvider.notifier).verifyTwoFactor(code); } } @@ -90,11 +95,14 @@ class _Verify2faScreenState extends ConsumerState { final authState = ref.watch(authProvider); final isLoading = authState.status == AuthStatus.loading; - // Navigate to home when authentication succeeds + // Navigate to home when authentication succeeds, reset submit lock on error ref.listen(authProvider, (previous, next) { if (next.status == AuthStatus.authenticated) { context.go('/home'); } + if (next.status != AuthStatus.loading) { + _isSubmitting = false; + } }); return Scaffold(