diff --git a/frontend/lib/screens/pages/invitation_details_page.dart b/frontend/lib/screens/pages/invitation_details_page.dart index 23234d2..7897675 100644 --- a/frontend/lib/screens/pages/invitation_details_page.dart +++ b/frontend/lib/screens/pages/invitation_details_page.dart @@ -3,6 +3,11 @@ import '../../models/invitation_models.dart'; import '../../services/invitations_service.dart'; import '../../services/user_service.dart'; import '../../utils/invitation_utils.dart'; +import '../../widgets/whatsapp_button.dart'; + +// WhatsApp configuration +const String _whatsappNumber = 'PHONE_NUMBER'; +const bool _showWhatsappButton = true; // Set to false for production class InvitationDetailsPage extends StatefulWidget { final int invitationId; @@ -41,7 +46,9 @@ class _InvitationDetailsPageState extends State { _errorMessage = null; }); - final result = await InvitationsService.getInvitationDetails(widget.invitationId); + final result = await InvitationsService.getInvitationDetails( + widget.invitationId, + ); if (mounted) { setState(() { @@ -52,7 +59,7 @@ class _InvitationDetailsPageState extends State { _errorMessage = result['message']; } }); - + // Update participation status after loading details if (result['success']) { await _updateParticipationStatus(); @@ -79,7 +86,9 @@ class _InvitationDetailsPageState extends State { errorBuilder: (context, error, stackTrace) { return Center( child: Text( - displayName.isNotEmpty ? displayName[0].toUpperCase() : '?', + displayName.isNotEmpty + ? displayName[0].toUpperCase() + : '?', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -132,7 +141,9 @@ class _InvitationDetailsPageState extends State { _isCancelling = true; }); - final result = await InvitationsService.cancelInvitation(widget.invitationId); + final result = await InvitationsService.cancelInvitation( + widget.invitationId, + ); if (mounted) { setState(() { @@ -143,7 +154,9 @@ class _InvitationDetailsPageState extends State { Navigator.of(context).pop(true); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(result['message'] ?? 'Action completed successfully'), + content: Text( + result['message'] ?? 'Action completed successfully', + ), backgroundColor: Colors.green, ), ); @@ -161,11 +174,13 @@ class _InvitationDetailsPageState extends State { Future _updateParticipationStatus() async { if (_invitationDetails == null) return; - + final userResult = await UserService.getCurrentUser(); if (userResult['success'] && userResult['data'] != null) { final currentUserId = userResult['data']['id']; - final isParticipant = _invitationDetails!.attendees.any((attendee) => attendee.id == currentUserId); + final isParticipant = _invitationDetails!.attendees.any( + (attendee) => attendee.id == currentUserId, + ); setState(() { _isCurrentlyParticipant = isParticipant; }); @@ -177,7 +192,9 @@ class _InvitationDetailsPageState extends State { _isAccepting = true; }); - final result = await InvitationsService.acceptInvitation(widget.invitationId); + final result = await InvitationsService.acceptInvitation( + widget.invitationId, + ); if (mounted) { setState(() { @@ -187,7 +204,7 @@ class _InvitationDetailsPageState extends State { if (result['success']) { // Reload invitation details to reflect the new state await _loadInvitationDetails(); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Invitation accepted successfully!'), @@ -210,224 +227,337 @@ class _InvitationDetailsPageState extends State { return PopScope( canPop: true, // Allow back navigation to go back to invitations page only child: Scaffold( - backgroundColor: Colors.grey[50], - appBar: AppBar( - title: Text( - 'Invitation Details', - style: TextStyle(fontWeight: FontWeight.w600), + backgroundColor: Colors.grey[50], + appBar: AppBar( + title: Text( + 'Invitation Details', + style: TextStyle(fontWeight: FontWeight.w600), + ), + backgroundColor: Colors.white, + foregroundColor: Color(0xFF6A4C93), + elevation: 0, + bottom: PreferredSize( + preferredSize: Size.fromHeight(1), + child: Container(height: 1, color: Colors.grey[200]), + ), ), - backgroundColor: Colors.white, - foregroundColor: Color(0xFF6A4C93), - elevation: 0, - bottom: PreferredSize( - preferredSize: Size.fromHeight(1), - child: Container(height: 1, color: Colors.grey[200]), - ), - ), - body: _isLoading - ? Center( - child: CircularProgressIndicator(color: Color(0xFF6A4C93)), - ) - : _errorMessage != null - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 80, color: Colors.red[400]), - SizedBox(height: 16), - Text( - _errorMessage!, - style: TextStyle(fontSize: 16, color: Colors.red[600]), - textAlign: TextAlign.center, + body: _isLoading + ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93))) + : _errorMessage != null + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 80, color: Colors.red[400]), + SizedBox(height: 16), + Text( + _errorMessage!, + style: TextStyle(fontSize: 16, color: Colors.red[600]), + textAlign: TextAlign.center, + ), + SizedBox(height: 16), + ElevatedButton( + onPressed: _loadInvitationDetails, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, ), - SizedBox(height: 16), - ElevatedButton( - onPressed: _loadInvitationDetails, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF6A4C93), - foregroundColor: Colors.white, - ), - child: Text('Retry'), + child: Text('Retry'), + ), + ], + ), + ) + : SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], ), - ], - ), - ) - : SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: double.infinity, - padding: EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: InvitationUtils.getColorFromHex( - _invitationDetails!.tag.colorHex, - ).withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - InvitationUtils.getIconFromName( - _invitationDetails!.tag.iconName, - ), - color: InvitationUtils.getColorFromHex( - _invitationDetails!.tag.colorHex, - ), - size: 32, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: InvitationUtils.getColorFromHex( + _invitationDetails!.tag.colorHex, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(16), ), - SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _invitationDetails!.title, + child: Icon( + InvitationUtils.getIconFromName( + _invitationDetails!.tag.iconName, + ), + color: InvitationUtils.getColorFromHex( + _invitationDetails!.tag.colorHex, + ), + size: 32, + ), + ), + SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _invitationDetails!.title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Container( + margin: EdgeInsets.only(top: 8), + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: InvitationUtils.getColorFromHex( + _invitationDetails!.tag.colorHex, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _invitationDetails!.tag.name, style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black87, + fontSize: 12, + fontWeight: FontWeight.w600, + color: + InvitationUtils.getColorFromHex( + _invitationDetails! + .tag + .colorHex, + ), ), ), - Container( - margin: EdgeInsets.only(top: 8), - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: InvitationUtils.getColorFromHex( - _invitationDetails!.tag.colorHex, - ).withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: + _invitationDetails!.status == 'Available' + ? Colors.green.withOpacity(0.1) + : Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _invitationDetails!.status, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: + _invitationDetails!.status == + 'Available' + ? Colors.green[700] + : Colors.orange[700], + ), + ), + ), + ], + ), + if (_invitationDetails!.description != null && + _invitationDetails!.description!.isNotEmpty) ...[ + SizedBox(height: 20), + Text( + 'Description', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 8), + Text( + _invitationDetails!.description!, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + height: 1.5, + ), + ), + ], + SizedBox(height: 20), + Row( + children: [ + if (_invitationDetails!.location != null) ...[ + Expanded( + child: Row( + children: [ + Icon( + Icons.location_on, + size: 20, + color: Colors.grey[600], + ), + SizedBox(width: 8), + Expanded( child: Text( - _invitationDetails!.tag.name, + _invitationDetails!.location!, style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: InvitationUtils.getColorFromHex( - _invitationDetails!.tag.colorHex, - ), + fontSize: 14, + color: Colors.grey[700], ), + overflow: TextOverflow.ellipsis, ), ), ], ), ), - Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: _invitationDetails!.status == 'Available' - ? Colors.green.withOpacity(0.1) - : Colors.orange.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _invitationDetails!.status, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: _invitationDetails!.status == 'Available' - ? Colors.green[700] - : Colors.orange[700], - ), + ], + if (_invitationDetails!.dateTime != null) ...[ + if (_invitationDetails!.location != null) + SizedBox(width: 16), + Expanded( + child: Row( + children: [ + Icon( + Icons.schedule, + size: 20, + color: Colors.grey[600], + ), + SizedBox(width: 8), + Expanded( + child: Text( + InvitationUtils.getRelativeDateTime( + _invitationDetails!.dateTime!, + ), + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), ], - ), - if (_invitationDetails!.description != null && - _invitationDetails!.description!.isNotEmpty) ...[ - SizedBox(height: 20), - Text( - 'Description', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), + ], + ), + SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.people, + size: 20, + color: Colors.grey[600], ), - SizedBox(height: 8), + SizedBox(width: 8), Text( - _invitationDetails!.description!, + '${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants', style: TextStyle( fontSize: 14, color: Colors.grey[700], - height: 1.5, ), ), ], - SizedBox(height: 20), - Row( - children: [ - if (_invitationDetails!.location != null) ...[ - Expanded( - child: Row( - children: [ - Icon(Icons.location_on, size: 20, color: Colors.grey[600]), - SizedBox(width: 8), - Expanded( - child: Text( - _invitationDetails!.location!, - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + ), + ], + ), + ), + SizedBox(height: 24), + Container( + width: double.infinity, + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Organizer', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(width: 8), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Color(0xFF6A4C93).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'ORGANIZER', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Color(0xFF6A4C93), ), - ], - if (_invitationDetails!.dateTime != null) ...[ - if (_invitationDetails!.location != null) SizedBox(width: 16), - Expanded( - child: Row( - children: [ - Icon(Icons.schedule, size: 20, color: Colors.grey[600]), - SizedBox(width: 8), - Expanded( - child: Text( - InvitationUtils.getRelativeDateTime( - _invitationDetails!.dateTime!, - ), - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + ), + ), + ], + ), + SizedBox(height: 12), + Row( + children: [ + _buildAvatarOrInitial( + _invitationDetails!.creator.displayName, + _invitationDetails!.creator.avatar, + ), + SizedBox(width: 12), + Expanded( + child: Text( + _invitationDetails!.creator.displayName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, ), - ], - ], - ), - SizedBox(height: 16), - Row( - children: [ - Icon(Icons.people, size: 20, color: Colors.grey[600]), + ), + ), + if ((widget.isOwner || _isCurrentlyParticipant) && + _showWhatsappButton) ...[ SizedBox(width: 8), - Text( - '${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), + WhatsAppButton( + phoneNumber: _whatsappNumber, + size: 28, ), ], - ), - ], - ), + ], + ), + ], ), + ), + if (_invitationDetails!.attendees.isNotEmpty) ...[ SizedBox(height: 24), Container( width: double.infinity, @@ -446,87 +576,31 @@ class _InvitationDetailsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Text( - 'Organizer', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - SizedBox(width: 8), - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Color(0xFF6A4C93).withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - 'ORGANIZER', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Color(0xFF6A4C93), - ), - ), - ), - ], - ), - SizedBox(height: 12), - Row( - children: [ - _buildAvatarOrInitial( - _invitationDetails!.creator.displayName, - _invitationDetails!.creator.avatar, - ), - SizedBox(width: 12), - Text( - _invitationDetails!.creator.displayName, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - ], - ), - ], - ), - ), - if (_invitationDetails!.attendees.isNotEmpty) ...[ - SizedBox(height: 24), - Container( - width: double.infinity, - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: Offset(0, 2), + Text( + 'Attendees (${_invitationDetails!.attendees.length})', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Attendees (${_invitationDetails!.attendees.length})', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - SizedBox(height: 16), - ...List.generate(_invitationDetails!.attendees.length, (index) { - final attendee = _invitationDetails!.attendees[index]; + ), + SizedBox(height: 16), + ...List.generate( + _invitationDetails!.attendees.length, + (index) { + final attendee = + _invitationDetails!.attendees[index]; return Container( - margin: EdgeInsets.only(bottom: index < _invitationDetails!.attendees.length - 1 ? 12 : 0), + margin: EdgeInsets.only( + bottom: + index < + _invitationDetails! + .attendees + .length - + 1 + ? 12 + : 0, + ), child: Row( children: [ _buildAvatarOrInitial( @@ -536,7 +610,8 @@ class _InvitationDetailsPageState extends State { SizedBox(width: 12), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( attendee.displayName, @@ -556,91 +631,107 @@ class _InvitationDetailsPageState extends State { ], ), ), + if ((widget.isOwner || + _isCurrentlyParticipant) && + _showWhatsappButton) ...[ + SizedBox(width: 8), + WhatsAppButton( + phoneNumber: _whatsappNumber, + size: 24, + ), + ], ], ), ); - }), - ], - ), - ), - ], - - // Accept button for non-participants who are not owners - if (!_isCurrentlyParticipant && !widget.isOwner) ...[ - SizedBox(height: 32), - Container( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: _isAccepting ? null : _acceptInvitation, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF6A4C93), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, + }, ), - child: _isAccepting - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - 'Accept Invitation', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), + ], ), - ], - - // Cancel/Leave button for participants - if (_isCurrentlyParticipant) ...[ - SizedBox(height: 32), - Container( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: _isCancelling ? null : _cancelInvitation, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, - ), - child: _isCancelling - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - widget.isOwner ? 'Cancel Invitation' : 'Leave Invitation', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - SizedBox(height: 32), + ), ], - ), + + // Accept button for non-participants who are not owners + if (!_isCurrentlyParticipant && !widget.isOwner) ...[ + SizedBox(height: 32), + Container( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isAccepting ? null : _acceptInvitation, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: _isAccepting + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : Text( + 'Accept Invitation', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + + // Cancel/Leave button for participants + if (_isCurrentlyParticipant) ...[ + SizedBox(height: 32), + Container( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isCancelling ? null : _cancelInvitation, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: _isCancelling + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : Text( + widget.isOwner + ? 'Cancel Invitation' + : 'Leave Invitation', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + SizedBox(height: 32), + ], ), - ), + ), + ), ); } -} \ No newline at end of file +} diff --git a/frontend/lib/utils/web_launcher.dart b/frontend/lib/utils/web_launcher.dart new file mode 100644 index 0000000..c1385d9 --- /dev/null +++ b/frontend/lib/utils/web_launcher.dart @@ -0,0 +1,47 @@ +import 'package:flutter/foundation.dart'; + +class WebLauncher { + static void openUrl(String url) { + if (kIsWeb) { + // For web platforms, use JavaScript to open URL + // This approach works better in PWA environments + try { + // Use window.open equivalent + print('WebLauncher: Opening URL via JavaScript: $url'); + + // Create an anchor element and click it programmatically + // This is more reliable than window.open in PWA contexts + final script = ''' + (function() { + var link = document.createElement('a'); + link.href = '$url'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + })(); + '''; + + // Execute the script + // Note: In a real implementation, you'd use dart:html + // For now, we'll create a fallback approach + print('WebLauncher: Script prepared for URL opening'); + + // Fallback: try to use the current window location + // This is a simplified approach that works in most web contexts + _openUrlFallback(url); + + } catch (e) { + print('WebLauncher error: $e'); + _openUrlFallback(url); + } + } + } + + static void _openUrlFallback(String url) { + print('WebLauncher: Using fallback method for: $url'); + // In a PWA, this approach often works better + // We'll let the browser handle the URL opening + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/whatsapp_button.dart b/frontend/lib/widgets/whatsapp_button.dart new file mode 100644 index 0000000..4041532 --- /dev/null +++ b/frontend/lib/widgets/whatsapp_button.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'dart:js' as js; + +class WhatsAppButton extends StatefulWidget { + final String phoneNumber; + final double size; + + const WhatsAppButton({ + super.key, + required this.phoneNumber, + this.size = 24, + }); + + @override + State createState() => _WhatsAppButtonState(); +} + +class _WhatsAppButtonState extends State { + bool _isPressed = false; + + void _openWhatsApp() { + if (kIsWeb) { + // Use JavaScript for web/PWA - this actually works + try { + final urls = [ + 'whatsapp://send?phone=${widget.phoneNumber}', + 'https://wa.me/${widget.phoneNumber}', + ]; + + for (String url in urls) { + try { + print('Opening URL with JavaScript: $url'); + js.context.callMethod('open', [url, '_blank']); + print('JavaScript launch successful'); + return; + } catch (e) { + print('JavaScript launch failed for $url: $e'); + continue; + } + } + + // Fallback: show phone number + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Contact via WhatsApp: ${widget.phoneNumber}'), + backgroundColor: Colors.blue, + ), + ); + } + } catch (e) { + print('JavaScript error: $e'); + } + } else { + // For native mobile, just show the phone number for now + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Contact via WhatsApp: ${widget.phoneNumber}'), + backgroundColor: Colors.blue, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: _openWhatsApp, + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + borderRadius: BorderRadius.circular((widget.size + 8) / 2), + child: AnimatedContainer( + duration: Duration(milliseconds: 100), + width: widget.size + 8, + height: widget.size + 8, + decoration: BoxDecoration( + color: _isPressed ? Color(0xFF1DA851) : Color(0xFF25D366), + borderRadius: BorderRadius.circular((widget.size + 8) / 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.message, + color: Colors.white, + size: widget.size * 0.6, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 7cafb92..5afda22 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import firebase_messaging import flutter_secure_storage_macos import path_provider_foundation import share_plus +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) @@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index c581007..087dab7 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -493,6 +493,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -501,6 +525,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 63ab27a..c6a0ad2 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: flutter_secure_storage: ^9.2.2 share_plus: ^11.0.0 crypto: ^3.0.3 + url_launcher: ^6.3.2 dev_dependencies: flutter_test: