diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b2f3367..b2448c1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index 692a7ce..176dea0 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -3,17 +3,21 @@ - - + + - + + android:gravity="center_horizontal" + android:top="80dp" + android:bottom="0dp"> diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 692a7ce..176dea0 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -3,17 +3,21 @@ - - + + - + + android:gravity="center_horizontal" + android:top="80dp" + android:bottom="0dp"> diff --git a/android/app/src/main/res/drawable/splash_house.png b/android/app/src/main/res/drawable/splash_house.png index 9946621..875187a 100644 Binary files a/android/app/src/main/res/drawable/splash_house.png and b/android/app/src/main/res/drawable/splash_house.png differ diff --git a/lib/features/home/presentation/widgets/home_header.dart b/lib/features/home/presentation/widgets/home_header.dart index 3b9e2a3..3c9dee2 100644 --- a/lib/features/home/presentation/widgets/home_header.dart +++ b/lib/features/home/presentation/widgets/home_header.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; +import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/notifications/presentation/providers/notification_provider.dart'; @@ -195,22 +196,16 @@ class _MenuDrawer extends ConsumerWidget { ), ), child: ClipOval( - child: user.avatar != null - ? Image.network( - user.avatar!, + child: user.avatar != null && + user.avatar!.isNotEmpty + ? S3Image( + imageUrl: user.avatar!, + width: 70, + height: 70, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - const Icon( - Icons.person, - size: 36, - color: AppColors.hintText, - ), + errorWidget: (_) => _buildInitialAvatar(user.firstName), ) - : const Icon( - Icons.person, - size: 36, - color: AppColors.hintText, - ), + : _buildInitialAvatar(user.firstName), ), ), const SizedBox(width: 12), @@ -488,6 +483,28 @@ class _MenuDrawer extends ConsumerWidget { ); } + Widget _buildInitialAvatar(String? firstName) { + final initial = (firstName != null && firstName.isNotEmpty) + ? firstName[0].toUpperCase() + : '?'; + return Container( + width: 70, + height: 70, + color: AppColors.primaryDark.withValues(alpha: 0.1), + child: Center( + child: Text( + initial, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 28, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + ), + ); + } + Widget _buildMenuItem( BuildContext context, { required IconData icon, diff --git a/lib/features/messaging/data/socket_service.dart b/lib/features/messaging/data/socket_service.dart index 2fb45f5..4f410f5 100644 --- a/lib/features/messaging/data/socket_service.dart +++ b/lib/features/messaging/data/socket_service.dart @@ -20,6 +20,9 @@ class SocketService { io.Socket? _socket; bool _isConnected = false; bool _intentionalDisconnect = false; + bool _isReconnecting = false; + int _reconnectAttempts = 0; + static const _maxReconnectAttempts = 3; String? _currentToken; // Stream controllers for events @@ -81,6 +84,8 @@ class SocketService { _socket!.onConnect((_) { _log.i('Socket connected: ${_socket!.id}'); _isConnected = true; + _reconnectAttempts = 0; // Reset on successful connect + _isReconnecting = false; _connectionController.add(true); }); @@ -89,9 +94,14 @@ class SocketService { _isConnected = false; _connectionController.add(false); - if (reason == 'io server disconnect' && !_intentionalDisconnect) { - _log.w('Server rejected connection - token likely expired'); - _reconnectWithFreshToken(); + if (reason == 'io server disconnect' && !_intentionalDisconnect && !_isReconnecting) { + if (_reconnectAttempts < _maxReconnectAttempts) { + _reconnectAttempts++; + _log.w('Server rejected connection (attempt $_reconnectAttempts/$_maxReconnectAttempts) - refreshing token...'); + _reconnectWithFreshToken(); + } else { + _log.e('Max reconnect attempts reached - giving up'); + } } }); @@ -170,31 +180,52 @@ class SocketService { } Future _reconnectWithFreshToken() async { - // Wait briefly for any in-flight REST token refresh to complete - await Future.delayed(const Duration(milliseconds: 500)); + _isReconnecting = true; + // Wait with exponential backoff + final delay = Duration(seconds: _reconnectAttempts * 2); + _log.i('Waiting ${delay.inSeconds}s before reconnect attempt...'); + await Future.delayed(delay); final token = await SecureStorage.getAccessToken(); - if (token == null) return; + if (token == null) { + _log.w('No access token available for socket reconnection'); + return; + } // If token hasn't changed, try triggering a refresh via a lightweight API call if (token == _currentToken) { + _log.i('Token unchanged, triggering refresh via /auth/me...'); try { - // Make a lightweight call that triggers the API client's 401 interceptor - // which auto-refreshes the token final dio = ApiClient.instance.dio; await dio.get('/auth/me'); final refreshedToken = await SecureStorage.getAccessToken(); - if (refreshedToken == null || refreshedToken == _currentToken) return; - _currentToken = refreshedToken; - } catch (_) { - return; // Token refresh failed — user will need to re-login + if (refreshedToken == null || refreshedToken == _currentToken) { + _log.w('Token refresh did not produce a new token'); + // Still try reconnecting — token might be valid but socket had a glitch + } else { + _log.i('Got fresh token after refresh'); + } + } catch (e) { + _log.e('Token refresh failed: $e'); + // Still try reconnecting with existing token } - } else { - _currentToken = token; } - _socket?.io.options?['auth'] = {'token': _currentToken}; - _socket?.connect(); + // Get the latest token (may have been refreshed) + final freshToken = await SecureStorage.getAccessToken(); + if (freshToken == null) return; + + _log.i('Reconnecting socket with fresh token...'); + // Full reconnect — dispose old socket and create new one + _socket?.disconnect(); + _socket?.dispose(); + _socket = null; + _isConnected = false; + _currentToken = null; + + // Small delay before reconnecting + await Future.delayed(const Duration(milliseconds: 500)); + await connect(); } /// Ensure socket is connected — reconnect if needed. @@ -205,6 +236,8 @@ class SocketService { void disconnect() { _intentionalDisconnect = true; + _isReconnecting = false; + _reconnectAttempts = 0; _socket?.disconnect(); _socket?.dispose(); _socket = null; diff --git a/lib/features/splash/presentation/screens/splash_screen.dart b/lib/features/splash/presentation/screens/splash_screen.dart index c613e25..954b8dd 100644 --- a/lib/features/splash/presentation/screens/splash_screen.dart +++ b/lib/features/splash/presentation/screens/splash_screen.dart @@ -75,23 +75,38 @@ class _SplashScreenState extends State ), child: FadeTransition( opacity: _fadeAnimation, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/splash_house.png', - width: 150, - fit: BoxFit.contain, - semanticLabel: 'House illustration', + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 150, + height: 108, + child: Image.asset( + 'assets/images/splash_house.png', + width: 150, + height: 108, + fit: BoxFit.contain, + semanticLabel: 'House illustration', + ), + ), + const SizedBox(height: 35), + SizedBox( + width: 264, + height: 55, + child: Image.asset( + 'assets/images/splash_logo.png', + width: 264, + height: 55, + fit: BoxFit.contain, + semanticLabel: 'RE-Quest logo', + ), + ), + ], ), - const SizedBox(height: 35), - Image.asset( - 'assets/images/splash_logo.png', - width: 264, - fit: BoxFit.contain, - semanticLabel: 'RE-Quest logo', - ), - ], + ), ), ), ),