11import 'package:google_sign_in/google_sign_in.dart' ;
22import 'package:supabase_flutter/supabase_flutter.dart' ;
33import 'package:flutter/foundation.dart' ;
4+ import 'dart:js' as js;
5+ import 'dart:async' ;
6+
7+ // NOTE: To fix the redirect_uri_mismatch error:
8+ // 1. Go to Google Cloud Console: https://console.cloud.google.com/
9+ // 2. Navigate to APIs & Services > Credentials
10+ // 3. Find your OAuth 2.0 Client ID and click Edit
11+ // 4. Under "Authorized redirect URIs", add these URIs:
12+ // - http://localhost
13+ // - http://localhost:3000
14+ // - https://your-domain.com (for production)
15+ // - The exact URI printed in the debug console when attempting sign-in
16+ // 5. Click Save and wait a few minutes for changes to propagate
417
518class GoogleAuthService {
6- final GoogleSignIn _googleSignIn = GoogleSignIn (
7- // The CLIENT_ID from the Google Cloud Console
8- clientId: kIsWeb ? '735463188812-jk93h4mn6l9pmphkm9vvggg7q4egpaai.apps.googleusercontent.com' : null ,
9- scopes: ['email' , 'profile' ],
10- );
19+ // Use a lazily initialized singleton pattern for GoogleSignIn
20+ static GoogleSignIn ? _instance;
21+
22+ // Get the GoogleSignIn instance, creating it if it doesn't exist
23+ GoogleSignIn get _googleSignIn {
24+ _instance ?? = GoogleSignIn (
25+ // The CLIENT_ID from the Google Cloud Console
26+ clientId: kIsWeb ? '735463188812-jk93h4mn6l9pmphkm9vvggg7q4egpaai.apps.googleusercontent.com' : null ,
27+ scopes: ['email' , 'profile' , 'openid' ],
28+ );
29+ return _instance! ;
30+ }
1131
1232 final SupabaseClient _supabaseClient = Supabase .instance.client;
1333
34+ /// Check if we were redirected from OAuth and handle the session
35+ Future <bool > checkForRedirectSession () async {
36+ if (kIsWeb) {
37+ try {
38+ debugPrint ('π§ͺ Checking for OAuth redirect...' );
39+
40+ // Get the current URL
41+ final url = Uri .base .toString ();
42+
43+ // Check for auth-related parameters in the URL
44+ final hasAuthParams = url.contains ('access_token=' ) ||
45+ url.contains ('refresh_token=' ) ||
46+ url.contains ('code=' );
47+
48+ if (hasAuthParams) {
49+ debugPrint ('π Found URL with auth parameters: $url ' );
50+
51+ // Simple approach: Just let Supabase handle the auth parameters
52+ try {
53+ // 1. Let Supabase handle the URL parsing
54+ await _supabaseClient.auth.getSessionFromUrl (Uri .parse (url));
55+
56+ // 2. Clear the URL
57+ _cleanUrl ();
58+
59+ // 3. Verify we have a session
60+ final hasSession = _supabaseClient.auth.currentSession != null ;
61+
62+ if (hasSession) {
63+ final user = _supabaseClient.auth.currentUser;
64+ debugPrint ('β
Successfully established auth session. User: ${user ?.email }' );
65+ return true ;
66+ } else {
67+ debugPrint ('β Failed to establish auth session despite valid URL parameters' );
68+ return false ;
69+ }
70+ } catch (e) {
71+ debugPrint ('β Error handling auth URL: $e ' );
72+ return false ;
73+ }
74+ }
75+
76+ // No auth parameters in URL, check if we have a session anyway
77+ final currentSession = _supabaseClient.auth.currentSession;
78+ if (currentSession != null ) {
79+ debugPrint ('β
No redirect detected, but found existing session for: ${currentSession .user .email }' );
80+ return true ;
81+ }
82+
83+ debugPrint ('β No auth parameters in URL and no existing session' );
84+ return false ;
85+ } catch (e) {
86+ debugPrint ('β Error in checkForRedirectSession: $e ' );
87+ return false ;
88+ }
89+ }
90+ return false ;
91+ }
92+
93+ /// Clean the URL by removing auth parameters
94+ void _cleanUrl () {
95+ if (kIsWeb) {
96+ try {
97+ // Get just the path portion of the current URL
98+ final cleanUrl = Uri .base .origin + Uri .base .path;
99+ js.context['history' ].callMethod ('replaceState' , [null , '' , cleanUrl]);
100+ debugPrint ('π§Ή URL cleaned: $cleanUrl ' );
101+ } catch (e) {
102+ debugPrint ('β Error cleaning URL: $e ' );
103+ }
104+ }
105+ }
106+
107+ /// Utility method to extract access token from URL
108+ String _extractTokenFromUrl (String url) {
109+ final uri = Uri .parse (url);
110+ final accessToken = uri.queryParameters['access_token' ];
111+ final refreshToken = uri.queryParameters['refresh_token' ];
112+
113+ debugPrint ('π URL contains access_token: ${accessToken != null }' );
114+ debugPrint ('π URL contains refresh_token: ${refreshToken != null }' );
115+
116+ if (accessToken != null ) {
117+ return accessToken;
118+ }
119+
120+ // For OAuth redirects, the token might be in the fragment
121+ final fragment = uri.fragment;
122+ if (fragment.isNotEmpty) {
123+ final params = Uri .splitQueryString (fragment);
124+ return params['access_token' ] ?? '' ;
125+ }
126+
127+ return '' ;
128+ }
129+
14130 Future <AuthResponse ?> signInWithGoogle () async {
15131 try {
16132 debugPrint ('Starting Google Sign In process' );
17133
18- // Trigger Google Sign In flow
134+ // For Web platform, use a different approach since signIn() is being deprecated
135+ if (kIsWeb) {
136+ return await _webSignIn ();
137+ }
138+
139+ // For mobile platforms, use the standard approach
19140 final GoogleSignInAccount ? googleUser = await _googleSignIn.signIn ();
20141 if (googleUser == null ) {
21142 debugPrint ('User canceled the sign-in flow' );
@@ -31,9 +152,12 @@ class GoogleAuthService {
31152 final idToken = googleAuth.idToken;
32153 final accessToken = googleAuth.accessToken;
33154
155+ debugPrint ('ID Token: ${idToken != null ? 'Present' : 'Not present' }' );
156+ debugPrint ('Access Token: ${accessToken != null ? 'Present' : 'Not present' }' );
157+
34158 if (idToken == null ) {
35- debugPrint ('No ID Token found' );
36- throw Exception ('No ID Token found' );
159+ debugPrint ('No ID Token found - this is likely due to browser cookie settings or missing gapi client ' );
160+ throw Exception ('No ID Token found. Please ensure third-party cookies are enabled in your browser settings. ' );
37161 }
38162
39163 debugPrint ('ID Token obtained, signing in with Supabase' );
@@ -54,6 +178,53 @@ class GoogleAuthService {
54178 }
55179 }
56180
181+ /// Web-specific Google Sign In approach using Supabase's OAuth flow
182+ Future <AuthResponse ?> _webSignIn () async {
183+ try {
184+ debugPrint ('Using Web-specific Google Sign In approach' );
185+
186+ // Get the current origin for our app to redirect back to
187+ String redirectUrl;
188+ final origin = Uri .base .origin;
189+
190+ // For production environments, we should use the actual deployed URL
191+ // This checks if we're running on localhost
192+ if (origin.contains ('localhost' )) {
193+ // Development environment
194+ redirectUrl = '$origin /' ;
195+ debugPrint ('π± DEVELOPMENT: Using localhost redirect: $redirectUrl ' );
196+ } else {
197+ // Production environment - use explicit production URL
198+ // Replace this with your actual production URL
199+ final productionUrl = origin;
200+ redirectUrl = '$productionUrl /' ;
201+ debugPrint ('π PRODUCTION: Using production redirect: $redirectUrl ' );
202+ }
203+
204+ debugPrint ('Will redirect back to: $redirectUrl ' );
205+
206+ // Use Supabase's built-in OAuth flow for Google
207+ final response = await _supabaseClient.auth.signInWithOAuth (
208+ OAuthProvider .google,
209+ // Specify where to redirect after Supabase completes the authentication
210+ redirectTo: redirectUrl,
211+ queryParams: {
212+ 'access_type' : 'offline' ,
213+ 'prompt' : 'consent' ,
214+ },
215+ );
216+
217+ debugPrint ('Supabase OAuth initiated successfully' );
218+
219+ // At this point, the user will be redirected to Google's login page,
220+ // and then back to your app via Supabase. The session will be handled by Supabase.
221+ return null ;
222+ } catch (e) {
223+ debugPrint ('Error in web sign-in: $e ' );
224+ rethrow ;
225+ }
226+ }
227+
57228 Future <void > signOut () async {
58229 try {
59230 await _googleSignIn.signOut ();
@@ -63,4 +234,46 @@ class GoogleAuthService {
63234 rethrow ;
64235 }
65236 }
237+
238+ /// Utility method to diagnose authentication issues
239+ Future <void > debugAuthState () async {
240+ try {
241+ debugPrint ('π AUTH DIAGNOSTICS π' );
242+
243+ // Check current session
244+ final currentSession = _supabaseClient.auth.currentSession;
245+ debugPrint ('π€ Has current session: ${currentSession != null }' );
246+
247+ if (currentSession != null ) {
248+ debugPrint ('π§ User email: ${currentSession .user .email ?? 'Not available' }' );
249+ debugPrint ('π User ID: ${currentSession .user .id }' );
250+ debugPrint ('β° Session expires at: ${currentSession .expiresAt != null ? DateTime .fromMillisecondsSinceEpoch (currentSession .expiresAt ! * 1000 ) : 'Unknown' }' );
251+
252+ // Check for access and refresh tokens
253+ debugPrint ('π Access token present: ${currentSession .accessToken .isNotEmpty }' );
254+ debugPrint ('π Refresh token present: ${currentSession .refreshToken ?.isNotEmpty ?? false }' );
255+ }
256+
257+ // Check current user
258+ final currentUser = _supabaseClient.auth.currentUser;
259+ debugPrint ('π€ Has current user: ${currentUser != null }' );
260+
261+ if (currentUser != null ) {
262+ debugPrint ('π§ User email: ${currentUser .email ?? 'Not available' }' );
263+ debugPrint ('π User ID: ${currentUser .id }' );
264+ debugPrint ('β
Email confirmed: ${currentUser .emailConfirmedAt != null }' );
265+ debugPrint ('π± Phone confirmed: ${currentUser .phoneConfirmedAt != null }' );
266+ }
267+
268+ // Check if these match
269+ if (currentSession != null && currentUser != null ) {
270+ final sessionMatchesUser = currentSession.user.id == currentUser.id;
271+ debugPrint ('π Session user matches current user: $sessionMatchesUser ' );
272+ }
273+
274+ debugPrint ('π END AUTH DIAGNOSTICS π' );
275+ } catch (e) {
276+ debugPrint ('β Error in debugAuthState: $e ' );
277+ }
278+ }
66279}
0 commit comments