-
-
Notifications
You must be signed in to change notification settings - Fork 111
Add Google Meet integration to meeting creation #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:intl/intl.dart'; | ||
| import '../../services/supabase_service.dart'; | ||
| import '../../services/google_meet_service.dart'; | ||
|
|
||
| class CreateMeetingScreen extends StatefulWidget { | ||
| const CreateMeetingScreen({super.key}); | ||
|
|
@@ -14,13 +15,15 @@ class _CreateMeetingScreenState extends State<CreateMeetingScreen> { | |
| final _titleController = TextEditingController(); | ||
| final _descriptionController = TextEditingController(); | ||
| final _urlController = TextEditingController(); | ||
| final _durationController = | ||
| TextEditingController(text: '60'); // Default to 60 minutes | ||
| final _durationController = TextEditingController(text: '60'); // Default to 60 minutes | ||
|
|
||
| final _supabaseService = SupabaseService(); | ||
|
|
||
| final _googleMeetService = GoogleMeetService(); | ||
|
|
||
| DateTime? _selectedDate; | ||
| TimeOfDay? _selectedTime; | ||
| bool _isLoading = false; | ||
| bool _isCreatingMeetLink = false; | ||
| bool _isGoogleMeetUrl = true; | ||
|
|
||
| @override | ||
|
|
@@ -44,7 +47,86 @@ class _CreateMeetingScreenState extends State<CreateMeetingScreen> { | |
| _isGoogleMeetUrl = _validateGoogleMeetUrl(url); | ||
| }); | ||
| } | ||
|
|
||
| Future<void> _createGoogleMeetLink() async { | ||
| if (_selectedDate == null || _selectedTime == null) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Please select date and time first'), | ||
| backgroundColor: Colors.red, | ||
| ), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Combine date and time | ||
| final meetingDateTime = DateTime( | ||
| _selectedDate!.year, | ||
| _selectedDate!.month, | ||
| _selectedDate!.day, | ||
| _selectedTime!.hour, | ||
| _selectedTime!.minute, | ||
| ); | ||
|
|
||
| // Validate meeting is in the future | ||
| if (meetingDateTime.isBefore(DateTime.now())) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Meeting time must be in the future'), | ||
| backgroundColor: Colors.red, | ||
| ), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Parse duration | ||
| int duration = 60; | ||
| try { | ||
| duration = int.parse(_durationController.text.trim()); | ||
| if (duration <= 0) duration = 60; | ||
| } catch (e) { | ||
| // Default to 60 if parsing fails | ||
| duration = 60; | ||
| } | ||
|
|
||
| setState(() => _isCreatingMeetLink = true); | ||
|
|
||
| final link = await _googleMeetService.createMeetLink( | ||
| start: meetingDateTime, | ||
| durationMinutes: duration, | ||
| title: _titleController.text.trim().isNotEmpty | ||
| ? _titleController.text.trim() | ||
| : 'Meeting', | ||
| description: _descriptionController.text.trim(), | ||
| ); | ||
|
|
||
| if (!mounted) return; | ||
|
|
||
| setState(() => _isCreatingMeetLink = false); | ||
|
|
||
| if (link != null) { | ||
| _urlController.text = link; | ||
| _checkUrl(link); | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar( | ||
| content: Text('Google Meet link created successfully'), | ||
| backgroundColor: Colors.green, | ||
| ), | ||
| ); | ||
| } else { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar( | ||
| content: const Text('Failed to create Google Meet link. Please sign in with Google.'), | ||
| backgroundColor: Colors.orange, | ||
| action: SnackBarAction( | ||
| label: 'Retry', | ||
| onPressed: () => _createGoogleMeetLink(), | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Future<void> _createMeeting() async { | ||
| if (!_formKey.currentState!.validate()) return; | ||
|
|
||
|
|
@@ -372,7 +454,33 @@ class _CreateMeetingScreenState extends State<CreateMeetingScreen> { | |
| _checkUrl(value); | ||
| }, | ||
| ), | ||
|
|
||
|
|
||
| // Create Google Meet Link button | ||
| const SizedBox(height: 8), | ||
| SizedBox( | ||
| width: double.infinity, | ||
| child: ElevatedButton.icon( | ||
| onPressed: _isCreatingMeetLink ? null : _createGoogleMeetLink, | ||
| icon: _isCreatingMeetLink | ||
| ? const SizedBox( | ||
| width: 16, | ||
| height: 16, | ||
| child: CircularProgressIndicator( | ||
| strokeWidth: 2, | ||
| color: Colors.white, | ||
| ), | ||
| ) | ||
| : const Icon(Icons.video_call), | ||
| label: Text( | ||
| _isCreatingMeetLink ? 'Creating Meet Link...' : 'Create Google Meet Link', | ||
| ), | ||
| style: ElevatedButton.styleFrom( | ||
| backgroundColor: Colors.blue.shade800, | ||
| padding: const EdgeInsets.symmetric(vertical: 16), | ||
| ), | ||
| ), | ||
| ), | ||
|
Comment on lines
+458
to
+482
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Button UI and loading state look correct. The button is properly disabled during link creation, shows a One minor UX note: the "Create Meeting" button (Line 512) is not disabled while Suggested fix for the Create Meeting button SizedBox(
width: double.infinity,
child: ElevatedButton(
- onPressed: _createMeeting,
+ onPressed: _isCreatingMeetLink ? null : _createMeeting,π€ Prompt for AI Agents |
||
|
|
||
| // Warning message for non-Google Meet URLs | ||
| if (_urlController.text.isNotEmpty && !_isGoogleMeetUrl) | ||
| Padding( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import 'package:google_sign_in/google_sign_in.dart'; | ||
| import 'package:googleapis/calendar/v3.dart' as cal; | ||
| import 'package:googleapis_auth/auth_io.dart'; | ||
| import 'package:http/http.dart' as http; | ||
|
|
||
| class GoogleMeetService { | ||
| final GoogleSignIn _googleSignIn = GoogleSignIn( | ||
| scopes: [ | ||
| cal.CalendarApi.calendarScope, | ||
| cal.CalendarApi.calendarEventsScope, | ||
| ], | ||
| ); | ||
|
|
||
| Future<String?> createMeetLink({ | ||
| required DateTime start, | ||
| required int durationMinutes, | ||
| required String title, | ||
| String? description, | ||
| }) async { | ||
| try { | ||
| // Sign in with Google | ||
| final account = await _googleSignIn.signIn(); | ||
| if (account == null) return null; | ||
|
|
||
| // Get authentication | ||
| final authentication = await account.authentication; | ||
| final accessToken = authentication.accessToken; | ||
|
|
||
| if (accessToken == null) { | ||
| print('No access token available'); | ||
| return null; | ||
| } | ||
|
|
||
| // Create authenticated client using authenticatedClient function (FIXED) | ||
| final credentials = AccessCredentials( | ||
| AccessToken('Bearer', accessToken, DateTime.now().add(const Duration(hours: 1)).toUtc()), | ||
|
Comment on lines
+35
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded token expiry is unreliable. The actual access token lifespan is determined by Google's auth server and may differ from the assumed 1-hour window. π€ Prompt for AI Agents |
||
| null, // No refresh token needed for one-time use | ||
| _googleSignIn.scopes, | ||
| ); | ||
|
|
||
| final authClient = authenticatedClient(http.Client(), credentials); | ||
|
|
||
| try { | ||
| final calendarApi = cal.CalendarApi(authClient); | ||
|
|
||
| final event = cal.Event( | ||
| summary: title, | ||
| description: description, | ||
| start: cal.EventDateTime(dateTime: start.toUtc()), | ||
| end: cal.EventDateTime( | ||
| dateTime: start.add(Duration(minutes: durationMinutes)).toUtc(), | ||
| ), | ||
| conferenceData: cal.ConferenceData( | ||
| createRequest: cal.CreateConferenceRequest( | ||
| requestId: DateTime.now().millisecondsSinceEpoch.toString(), | ||
| conferenceSolutionKey: cal.ConferenceSolutionKey( | ||
| type: 'hangoutsMeet', | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| final createdEvent = await calendarApi.events.insert( | ||
| event, | ||
| 'primary', | ||
| conferenceDataVersion: 1, | ||
| ); | ||
|
|
||
| // Return the meet link | ||
| return createdEvent.hangoutLink ?? | ||
| createdEvent.conferenceData?.entryPoints | ||
| ?.firstWhere( | ||
| (e) => e.entryPointType == 'video', | ||
| orElse: () => cal.EntryPoint(uri: ''), | ||
| ) | ||
| .uri; | ||
|
Comment on lines
+69
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fallback can return an empty string instead of When Suggested fix- return createdEvent.hangoutLink ??
- createdEvent.conferenceData?.entryPoints
- ?.firstWhere(
- (e) => e.entryPointType == 'video',
- orElse: () => cal.EntryPoint(uri: ''),
- )
- .uri;
+ final link = createdEvent.hangoutLink ??
+ createdEvent.conferenceData?.entryPoints
+ ?.firstWhere(
+ (e) => e.entryPointType == 'video',
+ orElse: () => cal.EntryPoint(),
+ )
+ .uri;
+ return (link != null && link.isNotEmpty) ? link : null;π€ Prompt for AI Agents |
||
| } finally { | ||
| authClient.close(); | ||
| } | ||
| } catch (e) { | ||
| print('Error creating Google Meet link: $e'); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| Future<void> signOut() async { | ||
| await _googleSignIn.signOut(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against empty-string Meet link from the service.
As noted in the service review,
createMeetLinkcan return""instead ofnull. Even after fixing the service, a defensive check here prevents a broken link from silently populating the URL field.Suggested fix
π Committable suggestion
π€ Prompt for AI Agents