feat: add an invitaiton URL with
This commit is contained in:
parent
251bb79f0a
commit
ab80afdc71
@ -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(() {
|
||||||
@ -52,7 +59,7 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
|||||||
_errorMessage = result['message'];
|
_errorMessage = result['message'];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update participation status after loading details
|
// Update participation status after loading details
|
||||||
if (result['success']) {
|
if (result['success']) {
|
||||||
await _updateParticipationStatus();
|
await _updateParticipationStatus();
|
||||||
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -161,11 +174,13 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
|||||||
|
|
||||||
Future<void> _updateParticipationStatus() async {
|
Future<void> _updateParticipationStatus() async {
|
||||||
if (_invitationDetails == null) return;
|
if (_invitationDetails == null) return;
|
||||||
|
|
||||||
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(() {
|
||||||
@ -187,7 +204,7 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
|||||||
if (result['success']) {
|
if (result['success']) {
|
||||||
// Reload invitation details to reflect the new state
|
// Reload invitation details to reflect the new state
|
||||||
await _loadInvitationDetails();
|
await _loadInvitationDetails();
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Invitation accepted successfully!'),
|
content: Text('Invitation accepted successfully!'),
|
||||||
@ -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),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
frontend/lib/utils/web_launcher.dart
Normal file
47
frontend/lib/utils/web_launcher.dart
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
102
frontend/lib/widgets/whatsapp_button.dart
Normal file
102
frontend/lib/widgets/whatsapp_button.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user