From 63288fa06a1a81f4cff51fdf2f0677b966e65a2a Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Thu, 9 Apr 2026 17:02:20 +0530 Subject: [PATCH] feat: implement user reporting in chat and dynamic environment-aware testimonial links --- .../presentation/screens/chat_screen.dart | 115 ++++++++++++++---- .../widgets/testimonials_tab.dart | 52 ++++++-- 2 files changed, 132 insertions(+), 35 deletions(-) diff --git a/lib/features/messaging/presentation/screens/chat_screen.dart b/lib/features/messaging/presentation/screens/chat_screen.dart index ea13e1a..a9dc610 100644 --- a/lib/features/messaging/presentation/screens/chat_screen.dart +++ b/lib/features/messaging/presentation/screens/chat_screen.dart @@ -4,7 +4,10 @@ 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:dio/dio.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; +import 'package:real_estate_mobile/core/network/api_client.dart'; +import 'package:real_estate_mobile/core/network/api_exceptions.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/auth/presentation/providers/auth_provider.dart'; @@ -349,38 +352,102 @@ class _ChatScreenState extends ConsumerState } void _showReportDialog() { - final controller = TextEditingController(); + final descController = TextEditingController(); + String selectedReason = 'Spam'; + final reasons = [ + 'Spam', + 'Harassment', + 'Inappropriate Content', + 'Scam / Fraud', + 'Other', + ]; + showDialog( context: context, - builder: (ctx) => AlertDialog( - title: const Text('Report User'), - content: TextField( - controller: controller, - maxLines: 3, - decoration: const InputDecoration( - hintText: 'Describe the issue...', - border: OutlineInputBorder(), + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => AlertDialog( + title: const Text('Report User'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Reason:', style: TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + DropdownButtonFormField( + value: selectedReason, + items: reasons.map((r) => DropdownMenuItem(value: r, child: Text(r))).toList(), + onChanged: (v) { + if (v != null) setDialogState(() => selectedReason = v); + }, + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: true, + ), + ), + const SizedBox(height: 12), + TextField( + controller: descController, + maxLines: 3, + decoration: const InputDecoration( + hintText: 'Additional details (optional)...', + border: OutlineInputBorder(), + ), + ), + ], ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.pop(ctx); + await _submitReport(selectedReason, descController.text.trim()); + }, + child: const Text('Submit'), + ), + ], ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.pop(ctx); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Report submitted. Thank you.')), - ); - }, - child: const Text('Submit'), - ), - ], ), ); } + Future _submitReport(String reason, String description) async { + // Find the other party's userId for the report + final conversations = ref.read(messagingProvider).conversations; + final conv = conversations.where((c) => c.id == widget.conversationId).firstOrNull; + if (conv == null) return; + + // otherParty.userId is the actual user ID (for agents), + // for regular users otherParty.id is the agentProfileId — we need the userId behind it + final reportedUserId = conv.otherParty.userId ?? conv.otherParty.id; + + try { + final dio = ApiClient.instance.dio; + await dio.post('/user-reports', data: { + 'reportedUserId': reportedUserId, + 'conversationId': widget.conversationId, + 'reason': reason, + if (description.isNotEmpty) 'description': description, + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Report submitted. Thank you.')), + ); + } + } catch (e) { + if (mounted) { + final msg = (e is DioException && e.error is ApiException) + ? (e.error as ApiException).message + : 'Failed to submit report'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(msg)), + ); + } + } + } + void _handleBack() { // deactivate() will clear activeConversationId — no need to do it here. if (Navigator.of(context).canPop()) { diff --git a/lib/features/profile/presentation/widgets/testimonials_tab.dart b/lib/features/profile/presentation/widgets/testimonials_tab.dart index f41a8be..6f036be 100644 --- a/lib/features/profile/presentation/widgets/testimonials_tab.dart +++ b/lib/features/profile/presentation/widgets/testimonials_tab.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:real_estate_mobile/config/app_config.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; class TestimonialsTab extends ConsumerStatefulWidget { const TestimonialsTab({super.key}); @@ -26,6 +28,22 @@ class _TestimonialsTabState extends ConsumerState { _loadTestimonials(); } + /// Derive the web app URL from the current API base URL. + /// api: https://beta.re-quest.com/api/v1 → web: https://beta.re-quest.com + /// api: https://prod.api.re-quest.com/api/v1 → web: https://prod.re-quest.com + String _getWebBaseUrl() { + final apiUrl = AppConfig.apiBaseUrl; // e.g. https://beta.re-quest.com/api/v1 + try { + final uri = Uri.parse(apiUrl); + final host = uri.host; // e.g. beta.re-quest.com or prod.api.re-quest.com + // Remove "api." prefix if present (prod uses subdomain-based API) + final webHost = host.replaceFirst('api.', ''); + return '${uri.scheme}://$webHost'; + } catch (_) { + return 'https://re-quest.com'; + } + } + Future _loadTestimonials() async { if (_isLoadingTestimonials) return; setState(() => _isLoadingTestimonials = true); @@ -46,8 +64,11 @@ class _TestimonialsTabState extends ConsumerState { try { final repo = ref.read(profileRepositoryProvider); final token = await repo.generateTestimonialLink(); + // Derive the web app URL from the API base URL so the link works + // correctly across all environments (dev, beta, prod). + final webBaseUrl = _getWebBaseUrl(); setState(() { - _testimonialLink = 'https://re-quest.co/testimonials/$token'; + _testimonialLink = '$webBaseUrl/testimonials/$token'; _isGeneratingLink = false; }); } catch (e) { @@ -139,16 +160,25 @@ class _TestimonialsTabState extends ConsumerState { child: Row( children: [ Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - _testimonialLink!, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontFamily: 'Fractul', - fontSize: 14, - fontWeight: FontWeight.w300, - color: Colors.black, + child: GestureDetector( + onTap: () async { + final uri = Uri.parse(_testimonialLink!); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + _testimonialLink!, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w300, + color: AppColors.accentOrange, + decoration: TextDecoration.underline, + ), ), ), ),