feat: add an invitaiton URL with

This commit is contained in:
sBubshait 2025-08-03 13:57:50 +03:00
parent 251bb79f0a
commit ab80afdc71
6 changed files with 629 additions and 354 deletions

View File

@ -3,6 +3,11 @@ import '../../models/invitation_models.dart';
import '../../services/invitations_service.dart'; import '../../services/invitations_service.dart';
import '../../services/user_service.dart'; import '../../services/user_service.dart';
import '../../utils/invitation_utils.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 { class InvitationDetailsPage extends StatefulWidget {
final int invitationId; final int invitationId;
@ -41,7 +46,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_errorMessage = null; _errorMessage = null;
}); });
final result = await InvitationsService.getInvitationDetails(widget.invitationId); final result = await InvitationsService.getInvitationDetails(
widget.invitationId,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -79,7 +86,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return Center( return Center(
child: Text( child: Text(
displayName.isNotEmpty ? displayName[0].toUpperCase() : '?', displayName.isNotEmpty
? displayName[0].toUpperCase()
: '?',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -132,7 +141,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_isCancelling = true; _isCancelling = true;
}); });
final result = await InvitationsService.cancelInvitation(widget.invitationId); final result = await InvitationsService.cancelInvitation(
widget.invitationId,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -143,7 +154,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(result['message'] ?? 'Action completed successfully'), content: Text(
result['message'] ?? 'Action completed successfully',
),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@ -165,7 +178,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
final userResult = await UserService.getCurrentUser(); final userResult = await UserService.getCurrentUser();
if (userResult['success'] && userResult['data'] != null) { if (userResult['success'] && userResult['data'] != null) {
final currentUserId = userResult['data']['id']; final currentUserId = userResult['data']['id'];
final isParticipant = _invitationDetails!.attendees.any((attendee) => attendee.id == currentUserId); final isParticipant = _invitationDetails!.attendees.any(
(attendee) => attendee.id == currentUserId,
);
setState(() { setState(() {
_isCurrentlyParticipant = isParticipant; _isCurrentlyParticipant = isParticipant;
}); });
@ -177,7 +192,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_isAccepting = true; _isAccepting = true;
}); });
final result = await InvitationsService.acceptInvitation(widget.invitationId); final result = await InvitationsService.acceptInvitation(
widget.invitationId,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -210,224 +227,337 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
return PopScope( return PopScope(
canPop: true, // Allow back navigation to go back to invitations page only canPop: true, // Allow back navigation to go back to invitations page only
child: Scaffold( child: Scaffold(
backgroundColor: Colors.grey[50], backgroundColor: Colors.grey[50],
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Invitation Details', 'Invitation Details',
style: TextStyle(fontWeight: FontWeight.w600), 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, body: _isLoading
foregroundColor: Color(0xFF6A4C93), ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
elevation: 0, : _errorMessage != null
bottom: PreferredSize( ? Center(
preferredSize: Size.fromHeight(1), child: Column(
child: Container(height: 1, color: Colors.grey[200]), mainAxisAlignment: MainAxisAlignment.center,
), children: [
), Icon(Icons.error_outline, size: 80, color: Colors.red[400]),
body: _isLoading SizedBox(height: 16),
? Center( Text(
child: CircularProgressIndicator(color: Color(0xFF6A4C93)), _errorMessage!,
) style: TextStyle(fontSize: 16, color: Colors.red[600]),
: _errorMessage != null textAlign: TextAlign.center,
? Center( ),
child: Column( SizedBox(height: 16),
mainAxisAlignment: MainAxisAlignment.center, ElevatedButton(
children: [ onPressed: _loadInvitationDetails,
Icon(Icons.error_outline, size: 80, color: Colors.red[400]), style: ElevatedButton.styleFrom(
SizedBox(height: 16), backgroundColor: Color(0xFF6A4C93),
Text( foregroundColor: Colors.white,
_errorMessage!,
style: TextStyle(fontSize: 16, color: Colors.red[600]),
textAlign: TextAlign.center,
), ),
SizedBox(height: 16), child: Text('Retry'),
ElevatedButton( ),
onPressed: _loadInvitationDetails, ],
style: ElevatedButton.styleFrom( ),
backgroundColor: Color(0xFF6A4C93), )
foregroundColor: Colors.white, : SingleChildScrollView(
), padding: EdgeInsets.all(16),
child: Text('Retry'), 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: [
: SingleChildScrollView( Row(
padding: EdgeInsets.all(16), children: [
child: Column( Container(
crossAxisAlignment: CrossAxisAlignment.start, padding: EdgeInsets.all(16),
children: [ decoration: BoxDecoration(
Container( color: InvitationUtils.getColorFromHex(
width: double.infinity, _invitationDetails!.tag.colorHex,
padding: EdgeInsets.all(24), ).withOpacity(0.1),
decoration: BoxDecoration( borderRadius: BorderRadius.circular(16),
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,
),
), ),
SizedBox(width: 16), child: Icon(
Expanded( InvitationUtils.getIconFromName(
child: Column( _invitationDetails!.tag.iconName,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ color: InvitationUtils.getColorFromHex(
Text( _invitationDetails!.tag.colorHex,
_invitationDetails!.title, ),
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( style: TextStyle(
fontSize: 24, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
color: Colors.black87, color:
InvitationUtils.getColorFromHex(
_invitationDetails!
.tag
.colorHex,
),
), ),
), ),
Container( ),
margin: EdgeInsets.only(top: 8), ],
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), ),
decoration: BoxDecoration( ),
color: InvitationUtils.getColorFromHex( Container(
_invitationDetails!.tag.colorHex, padding: EdgeInsets.symmetric(
).withOpacity(0.1), horizontal: 12,
borderRadius: BorderRadius.circular(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( child: Text(
_invitationDetails!.tag.name, _invitationDetails!.location!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 14,
fontWeight: FontWeight.w600, color: Colors.grey[700],
color: InvitationUtils.getColorFromHex(
_invitationDetails!.tag.colorHex,
),
), ),
overflow: TextOverflow.ellipsis,
), ),
), ),
], ],
), ),
), ),
Container( ],
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), if (_invitationDetails!.dateTime != null) ...[
decoration: BoxDecoration( if (_invitationDetails!.location != null)
color: _invitationDetails!.status == 'Available' SizedBox(width: 16),
? Colors.green.withOpacity(0.1) Expanded(
: Colors.orange.withOpacity(0.1), child: Row(
borderRadius: BorderRadius.circular(12), children: [
), Icon(
child: Text( Icons.schedule,
_invitationDetails!.status, size: 20,
style: TextStyle( color: Colors.grey[600],
fontSize: 12, ),
fontWeight: FontWeight.w600, SizedBox(width: 8),
color: _invitationDetails!.status == 'Available' Expanded(
? Colors.green[700] child: Text(
: Colors.orange[700], InvitationUtils.getRelativeDateTime(
), _invitationDetails!.dateTime!,
),
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
overflow: TextOverflow.ellipsis,
),
),
],
), ),
), ),
], ],
), ],
if (_invitationDetails!.description != null && ),
_invitationDetails!.description!.isNotEmpty) ...[ SizedBox(height: 16),
SizedBox(height: 20), Row(
Text( children: [
'Description', Icon(
style: TextStyle( Icons.people,
fontSize: 16, size: 20,
fontWeight: FontWeight.w600, color: Colors.grey[600],
color: Colors.black87,
),
), ),
SizedBox(height: 8), SizedBox(width: 8),
Text( Text(
_invitationDetails!.description!, '${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[700], color: Colors.grey[700],
height: 1.5,
), ),
), ),
], ],
SizedBox(height: 20), ),
Row( ],
children: [ ),
if (_invitationDetails!.location != null) ...[ ),
Expanded( SizedBox(height: 24),
child: Row( Container(
children: [ width: double.infinity,
Icon(Icons.location_on, size: 20, color: Colors.grey[600]), padding: EdgeInsets.all(20),
SizedBox(width: 8), decoration: BoxDecoration(
Expanded( color: Colors.white,
child: Text( borderRadius: BorderRadius.circular(16),
_invitationDetails!.location!, boxShadow: [
style: TextStyle(fontSize: 14, color: Colors.grey[700]), BoxShadow(
overflow: TextOverflow.ellipsis, 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( SizedBox(height: 12),
children: [ Row(
Icon(Icons.schedule, size: 20, color: Colors.grey[600]), children: [
SizedBox(width: 8), _buildAvatarOrInitial(
Expanded( _invitationDetails!.creator.displayName,
child: Text( _invitationDetails!.creator.avatar,
InvitationUtils.getRelativeDateTime( ),
_invitationDetails!.dateTime!, SizedBox(width: 12),
), Expanded(
style: TextStyle(fontSize: 14, color: Colors.grey[700]), child: Text(
overflow: TextOverflow.ellipsis, _invitationDetails!.creator.displayName,
), style: TextStyle(
), fontSize: 16,
], fontWeight: FontWeight.w600,
), color: Colors.black87,
), ),
], ),
], ),
), if ((widget.isOwner || _isCurrentlyParticipant) &&
SizedBox(height: 16), _showWhatsappButton) ...[
Row(
children: [
Icon(Icons.people, size: 20, color: Colors.grey[600]),
SizedBox(width: 8), SizedBox(width: 8),
Text( WhatsAppButton(
'${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants', phoneNumber: _whatsappNumber,
style: TextStyle(fontSize: 14, color: Colors.grey[700]), size: 28,
), ),
], ],
), ],
], ),
), ],
), ),
),
if (_invitationDetails!.attendees.isNotEmpty) ...[
SizedBox(height: 24), SizedBox(height: 24),
Container( Container(
width: double.infinity, width: double.infinity,
@ -446,87 +576,31 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
children: [ 'Attendees (${_invitationDetails!.attendees.length})',
Text( style: TextStyle(
'Organizer', fontSize: 18,
style: TextStyle( fontWeight: FontWeight.bold,
fontSize: 18, color: Colors.black87,
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),
), ),
], ),
), SizedBox(height: 16),
child: Column( ...List.generate(
crossAxisAlignment: CrossAxisAlignment.start, _invitationDetails!.attendees.length,
children: [ (index) {
Text( final attendee =
'Attendees (${_invitationDetails!.attendees.length})', _invitationDetails!.attendees[index];
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 16),
...List.generate(_invitationDetails!.attendees.length, (index) {
final attendee = _invitationDetails!.attendees[index];
return Container( 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( child: Row(
children: [ children: [
_buildAvatarOrInitial( _buildAvatarOrInitial(
@ -536,7 +610,8 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
SizedBox(width: 12), SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Text( Text(
attendee.displayName, attendee.displayName,
@ -556,91 +631,107 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
], ],
), ),
), ),
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<Color>(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<Color>(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<Color>(
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<Color>(
Colors.white,
),
),
)
: Text(
widget.isOwner
? 'Cancel Invitation'
: 'Leave Invitation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
SizedBox(height: 32),
],
), ),
), ),
),
); );
} }
} }

View File

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

View File

@ -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<WhatsAppButton> createState() => _WhatsAppButtonState();
}
class _WhatsAppButtonState extends State<WhatsAppButton> {
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,
),
),
),
);
}
}

View File

@ -10,6 +10,7 @@ import firebase_messaging
import flutter_secure_storage_macos import flutter_secure_storage_macos
import path_provider_foundation import path_provider_foundation
import share_plus import share_plus
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -493,6 +493,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -501,6 +525,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" 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: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:

View File

@ -17,6 +17,7 @@ dependencies:
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
share_plus: ^11.0.0 share_plus: ^11.0.0
crypto: ^3.0.3 crypto: ^3.0.3
url_launcher: ^6.3.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: