feat: enhance user avatar display with S3Image and initial-based fallback, improve socket reconnection logic, and update splash screen layout, assets, and Android permissions.

This commit is contained in:
pradeepkumar
2026-03-27 15:39:38 +05:30
parent ca4315b09d
commit 625db4bb99
7 changed files with 132 additions and 56 deletions

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

View File

@@ -3,17 +3,21 @@
<!-- Gradient background from #c4d9d4 (top) to #f0f5fc (bottom) -->
<item android:drawable="@drawable/splash_gradient" />
<!-- House illustration centered -->
<item>
<!-- House illustration — above center -->
<item
android:gravity="center_horizontal"
android:top="0dp"
android:bottom="80dp">
<bitmap
android:gravity="center"
android:src="@drawable/splash_house" />
</item>
<!-- RE-Quest logo below center -->
<!-- RE-Quest logo below center -->
<item
android:gravity="center_horizontal|bottom"
android:bottom="200dp">
android:gravity="center_horizontal"
android:top="80dp"
android:bottom="0dp">
<bitmap
android:gravity="center"
android:src="@drawable/splash_logo" />

View File

@@ -3,17 +3,21 @@
<!-- Gradient background from #c4d9d4 (top) to #f0f5fc (bottom) -->
<item android:drawable="@drawable/splash_gradient" />
<!-- House illustration centered -->
<item>
<!-- House illustration — above center -->
<item
android:gravity="center_horizontal"
android:top="0dp"
android:bottom="80dp">
<bitmap
android:gravity="center"
android:src="@drawable/splash_house" />
</item>
<!-- RE-Quest logo below center -->
<!-- RE-Quest logo below center -->
<item
android:gravity="center_horizontal|bottom"
android:bottom="200dp">
android:gravity="center_horizontal"
android:top="80dp"
android:bottom="0dp">
<bitmap
android:gravity="center"
android:src="@drawable/splash_logo" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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,

View File

@@ -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');
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<void> _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 {
_currentToken = token;
_log.i('Got fresh token after refresh');
}
} catch (e) {
_log.e('Token refresh failed: $e');
// Still try reconnecting with existing 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;

View File

@@ -75,26 +75,41 @@ class _SplashScreenState extends State<SplashScreen>
),
child: FadeTransition(
opacity: _fadeAnimation,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
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),
Image.asset(
SizedBox(
width: 264,
height: 55,
child: Image.asset(
'assets/images/splash_logo.png',
width: 264,
height: 55,
fit: BoxFit.contain,
semanticLabel: 'RE-Quest logo',
),
),
],
),
),
),
),
),
);
}
}