150 lines
5.5 KiB
Markdown
150 lines
5.5 KiB
Markdown
# Firebase Auth & Google Sign-In for Flutter
|
|
|
|
When integrating Firebase Authentication and Google Sign-In into Flutter apps
|
|
targeting cross-platform environments (like Mobile + Web), you must navigate
|
|
several breaking changes introduced in `google_sign_in` 7.x+ and some
|
|
platform-specific quirks.
|
|
|
|
## 1. `google_sign_in` 7.2.0 API Changes
|
|
|
|
- **Method Renamed**: The `signIn()` method is deprecated/removed and has been
|
|
replaced with `authenticate()`.
|
|
- **Token Separation**: The `GoogleSignInAuthentication` object no longer
|
|
packages both identity and authorization tokens together. Initial
|
|
authentication now only provides the `idToken`. If an `accessToken` is
|
|
required for Google APIs, you must explicitly request server authorization
|
|
separately.
|
|
|
|
## 2. Initialization & Web Hang/Crash Pitfalls
|
|
|
|
- **Initialization Requirement**: In 7.x, you must call
|
|
`await GoogleSignIn.instance.initialize();` globally before using the plugin.
|
|
- **Web Client ID Constraint**: On Flutter Web, if you call `initialize()`
|
|
without passing a `clientId` argument OR specifying the
|
|
`<meta name="google-signin-client_id" ... />` tag in `web/index.html`, the
|
|
Dart Web Debug Service (DWDS) and the app will throw an assertion error and
|
|
**hang infinitely**, resulting in a blank screen.
|
|
- **Common Workaround**: If you intend to use Firebase Auth's
|
|
`signInWithPopup(GoogleAuthProvider())` for the web, you can conditionally
|
|
skip the local `GoogleSignIn` package initialization entirely:
|
|
```dart
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
|
if (!kIsWeb) {
|
|
await GoogleSignIn.instance.initialize();
|
|
}
|
|
```
|
|
|
|
## 3. Web Logout Crashes
|
|
|
|
- If you bypassed `GoogleSignIn` initialization on the web (as demonstrated
|
|
above), you cannot call its `signOut()` method later. Attempting to execute
|
|
`await GoogleSignIn.instance.signOut();` during the user's logout flow on the
|
|
Web platform evaluates against an uninitialized context or unsupported
|
|
environment, crashing the app.
|
|
- **Solution**: Conditionally separate the logout logic for Web to rely entirely
|
|
on `FirebaseAuth`:
|
|
```dart
|
|
if (!kIsWeb) {
|
|
await GoogleSignIn.instance.signOut();
|
|
}
|
|
await FirebaseAuth.instance.signOut();
|
|
```
|
|
|
|
## 4. Prototyping Workaround: Bypassing Firestore Composite Indices
|
|
|
|
*Note: This is a Firestore consideration frequently encountered while fetching
|
|
user-specific auth data.*
|
|
|
|
When querying data via `FirebaseFirestore.instance`, using
|
|
`.where('userId', isEqualTo: uid)` combined with a sort on a different field
|
|
like `.orderBy('createdAt', descending: true)` mandates a custom composite
|
|
index.
|
|
|
|
- **Quick Alternative**: During local development, you can avoid defining
|
|
indexes by pulling the data using only `.where()` and applying the `.sort()`
|
|
operation client-side on the resulting `List` in Dart.
|
|
|
|
## 5. Robust `AuthService` Boilerplate
|
|
|
|
Here is a comprehensive `AuthService` implementation that properly handles the
|
|
initialization and platform differences between Flutter Web and Mobile:
|
|
|
|
```dart
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:google_sign_in/google_sign_in.dart';
|
|
|
|
class AuthService {
|
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
|
|
|
AuthService() {
|
|
if (!kIsWeb) {
|
|
GoogleSignIn.instance.initialize();
|
|
}
|
|
}
|
|
|
|
// Stream to listen to auth state changes
|
|
Stream<User?> get authStateChanges => _auth.authStateChanges();
|
|
|
|
// Get current user
|
|
User? get currentUser => _auth.currentUser;
|
|
|
|
// Google Sign-In
|
|
Future<UserCredential?> signInWithGoogle() async {
|
|
try {
|
|
if (kIsWeb) {
|
|
// Web uses popup to avoid DWDS hangs and manual client ID config
|
|
GoogleAuthProvider authProvider = GoogleAuthProvider();
|
|
return await _auth.signInWithPopup(authProvider);
|
|
} else {
|
|
// Mobile uses standard flow
|
|
final GoogleSignInAccount? googleUser = await GoogleSignIn.instance.authenticate();
|
|
if (googleUser == null) return null; // Cancelled
|
|
|
|
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
|
|
|
|
final AuthCredential credential = GoogleAuthProvider.credential(
|
|
idToken: googleAuth.idToken,
|
|
);
|
|
|
|
return await _auth.signInWithCredential(credential);
|
|
}
|
|
} catch (e) {
|
|
print("Error during Google Sign-In: \$e");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Sign out
|
|
Future<void> signOut() async {
|
|
try {
|
|
if (!kIsWeb) {
|
|
await GoogleSignIn.instance.signOut();
|
|
}
|
|
await _auth.signOut();
|
|
} catch (e) {
|
|
print("Error signing out: \$e");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 6. Troubleshooting `auth/unauthorized-domain` on Flutter Web
|
|
|
|
When running Flutter Web locally and using `signInWithPopup`, you might
|
|
encounter a situation where the Google Sign-In popup opens and immediately
|
|
closes.
|
|
|
|
- **Symptom**: The console shows
|
|
`Sign-in failed: [firebase_auth/unauthorized-domain] This domain is not authorized for OAuth operation for your Firebase project.`
|
|
- **Cause**: The domain (usually `localhost` during local testing) is not listed
|
|
in the Authorized Domains in the Firebase Console.
|
|
- **Solution**: Add `localhost` to the Authorized Domains list in the Firebase
|
|
Console (Authentication > Settings > Authorized domains) or in your
|
|
`firebase.json` auth config.
|
|
- **CRITICAL**: Do NOT include the protocol or port number when adding the
|
|
domain (e.g., use `localhost`, NOT `http://localhost:9090`). Flutter Web often
|
|
runs on random ports or specific ports, but Firebase Auth only cares about the
|
|
domain.
|