diff --git a/lib/core/utils/image_url_resolver.dart b/lib/core/utils/image_url_resolver.dart index d51d2aa..dd01041 100644 --- a/lib/core/utils/image_url_resolver.dart +++ b/lib/core/utils/image_url_resolver.dart @@ -1,4 +1,3 @@ -import 'package:dio/dio.dart'; import 'package:real_estate_mobile/core/network/api_client.dart'; /// Resolves S3 keys to presigned download URLs via the backend API. @@ -54,10 +53,16 @@ class ImageUrlResolver { return data['url'] as String?; } return null; - } on DioException { + } catch (_) { return null; } } void clearCache() => _cache.clear(); + + /// Evict a single key from the in-memory cache so the next resolve + /// fetches a fresh presigned URL. + void evict(String? key) { + if (key != null) _cache.remove(key); + } } diff --git a/lib/core/widgets/s3_image.dart b/lib/core/widgets/s3_image.dart index c39a9b5..c392f88 100644 --- a/lib/core/widgets/s3_image.dart +++ b/lib/core/widgets/s3_image.dart @@ -60,13 +60,17 @@ class _S3ImageState extends State { return; } - final url = await ImageUrlResolver.instance.resolve(widget.imageUrl); - if (mounted) { - setState(() { - _resolvedUrl = url; - _loading = false; - _failed = url == null; - }); + try { + final url = await ImageUrlResolver.instance.resolve(widget.imageUrl); + if (mounted) { + setState(() { + _resolvedUrl = url; + _loading = false; + _failed = url == null; + }); + } + } catch (_) { + if (mounted) setState(() { _loading = false; _failed = true; }); } } @@ -102,12 +106,13 @@ class _S3ImageState extends State { return CachedNetworkImage( imageUrl: _resolvedUrl!, + cacheKey: widget.imageUrl, width: widget.width, height: widget.height, fit: widget.fit, alignment: widget.alignment, placeholder: (ctx, _) => _buildPlaceholder(ctx), - errorWidget: (ctx, _, __) => _buildError(ctx), + errorWidget: (ctx, _, _) => _buildError(ctx), ); } } diff --git a/lib/features/profile/presentation/widgets/change_password_tab.dart b/lib/features/profile/presentation/widgets/change_password_tab.dart index 865dc37..91de3b0 100644 --- a/lib/features/profile/presentation/widgets/change_password_tab.dart +++ b/lib/features/profile/presentation/widgets/change_password_tab.dart @@ -126,33 +126,32 @@ class _ChangePasswordTabState extends ConsumerState { ), ), const SizedBox(height: 14), - SizedBox( - height: 38, - child: TextField( - controller: controller, - obscureText: obscure, - style: const TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 14, - fontWeight: FontWeight.w400, - color: AppColors.primaryDark, - ), - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 26, vertical: 8), - border: border, - enabledBorder: border, - focusedBorder: border, - suffixIcon: GestureDetector( - onTap: onToggle, - child: Icon( - obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined, - size: 18, - color: AppColors.primaryDark.withValues(alpha: 0.5), - ), + TextField( + controller: controller, + obscureText: obscure, + textAlignVertical: TextAlignVertical.center, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark, + ), + decoration: InputDecoration( + isDense: true, + contentPadding: + const EdgeInsets.fromLTRB(12, 12, 8, 12), + border: border, + enabledBorder: border, + focusedBorder: border, + suffixIcon: GestureDetector( + onTap: onToggle, + child: Icon( + obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined, + size: 18, + color: AppColors.primaryDark.withValues(alpha: 0.5), ), - suffixIconConstraints: const BoxConstraints(minWidth: 40), ), + suffixIconConstraints: const BoxConstraints(minWidth: 40), ), ), ], diff --git a/lib/features/profile/presentation/widgets/profile_settings_tab.dart b/lib/features/profile/presentation/widgets/profile_settings_tab.dart index a66c9e5..1bbf642 100644 --- a/lib/features/profile/presentation/widgets/profile_settings_tab.dart +++ b/lib/features/profile/presentation/widgets/profile_settings_tab.dart @@ -1,10 +1,12 @@ import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; +import 'package:real_estate_mobile/core/utils/image_url_resolver.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/features/profile/data/profile_repository.dart'; import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart'; @@ -99,6 +101,13 @@ class _ProfileSettingsTabState extends ConsumerState { avatarUrl = user?['avatar'] as String?; } + // Evict stale presigned URL cache and disk cache for the avatar + // so S3Image always resolves a fresh presigned URL on profile load. + if (avatarUrl != null && avatarUrl.isNotEmpty) { + ImageUrlResolver.instance.evict(avatarUrl); + CachedNetworkImage.evictFromCache(avatarUrl); + } + final descriptionText = isAgent ? 'This information will be displayed on your public agent page.' : 'This information will be displayed on your profile.';