blob: 580d375c1c0d81b42bf70bdf604d23a3abcdc0ba [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart' show ChangeNotifier, debugPrint;
/// Service class for interacting with Google Sign In authentication for Cocoon backend.
///
/// Almost all operations with the plugin can throw an exception and should be caught
/// to prevent this service from crashing.
class FirebaseAuthService extends ChangeNotifier {
final FirebaseAuth _auth;
User? _user;
FirebaseAuthService({FirebaseAuth? auth})
: _auth = auth ?? FirebaseAuth.instance {
_auth.authStateChanges().listen((User? user) {
_user = user;
if (_user == null) {
print('user signed out');
} else {
print('user signed in');
}
notifyListeners();
});
}
User? get user {
return _user;
}
/// Whether or not the application has been signed in to.
///
/// If the plugin fails, default to unauthenticated.
bool get isAuthenticated {
return _user != null;
}
/// Authentication token to be sent to Cocoon Backend to verify API calls.
Future<String> get idToken async {
assert(
isAuthenticated,
'Ensure user isAuthenticated before requesting an idToken.',
);
final idToken = await _user?.getIdToken();
if (idToken == null || idToken.isEmpty) {
throw StateError('invalid idToken');
}
return idToken;
}
/// Initiate the Google Sign In process.
Future<void> signInWithGoogle() async {
await _signInWithGoogle();
notifyListeners();
}
Future<void> _signInWithGoogle() async {
try {
final userCredential = await _auth.signInWithPopup(GoogleAuthProvider());
_user = userCredential.user;
} catch (error) {
debugPrint('signin failed: $error');
}
}
/// Initiate the GitHub Sign In process.
Future<void> signInWithGithub() async {
await _signInWithGithub();
notifyListeners();
}
Future<void> _signInWithGithub() async {
try {
final userCredential = await _auth.signInWithPopup(GithubAuthProvider());
_user = userCredential.user;
} on FirebaseAuthException catch (error) {
// If email of Github account already registered in Frebase but with
// Google provider, we need to sign in with Google provider first,
// then link the GitHub provider to Google provider.
if (error.code == 'account-exists-with-different-credential') {
debugPrint('google account exists, signing in with google');
await _signInWithGoogle();
await _linkWithGithub();
return;
}
debugPrint('signin with github failed: $error');
} catch (error) {
debugPrint('signin with github failed: $error');
}
}
/// Sign out the currently signed in user.
Future<void> signOut() async {
await _signOut();
notifyListeners();
}
Future<void> _signOut() async {
try {
await _auth.signOut();
_user = null;
} catch (error) {
debugPrint('signout error $error');
}
}
/// Link the Google provider to the currently signed in user.
///
/// This method tries to keep Google account as primary provider.
Future<void> linkWithGoogle() async {
await _linkWithGoogle();
await _user?.getIdToken(true);
notifyListeners();
}
Future<void> _linkWithGoogle() async {
//Try to link google provider first.
try {
final userCredential = await _auth.currentUser?.linkWithPopup(
GoogleAuthProvider(),
);
_user = userCredential?.user;
notifyListeners();
await _auth.currentUser?.getIdToken(true);
} on FirebaseAuthException catch (error) {
// If Github account's credential already exists in firebase, we going to
// link google account to github.
if (error.code == 'credential-already-in-use') {
await _relinkGithubToGoogle();
return;
}
debugPrint('linkWithGoogle failed: $error');
} catch (error) {
debugPrint('linkWithGoogle failed: $error');
}
// If linking google succeeded, we need to unlink it and relink
// github to google to make google primary.
await _unlinkGoogle();
await _relinkGithubToGoogle();
}
Future<void> _relinkGithubToGoogle() async {
// We want to have Googole account Primary if present in firestore or linked,
// so we try to:
// 1. Delete GitHub account from firebase records, but to avoid
// **requires-recent-login** error we need to re-sign-in first;
try {
await _signOut();
await _signInWithGithub();
await FirebaseAuth.instance.currentUser?.delete();
} catch (e) {
debugPrint('delete user failed: $e');
return;
}
// 2. sign in with Google;
try {
await _signInWithGoogle();
} catch (error) {
debugPrint('signInWithGoogle failed: $error');
return;
}
// 3. then link GitHub account to existing Google account.
try {
await _linkWithGithub();
} catch (error) {
debugPrint('linkWithGoogle failed: $error');
}
}
/// Link the Github provider to the currently signed in user.
Future<void> linkWithGithub() async {
await _linkWithGithub();
await _user?.getIdToken(true);
notifyListeners();
}
Future<void> _linkWithGithub() async {
try {
final userCredential = await _auth.currentUser?.linkWithPopup(
GithubAuthProvider(),
);
_user = userCredential?.user;
} catch (error) {
debugPrint('linkWithGithub failed: $error');
//
}
}
/// Unlink the Github provider from the currently signed in user.
Future<void> unlinkGithub() async {
await _unlinkGithub();
await _user?.getIdToken(true);
notifyListeners();
}
Future<void> _unlinkGithub() async {
// Since google acount should be primary if linked to github,
// but single account should be github, after unlinking github we have to:
// delete google account and re-signin with github.
// 1. Unlink github provider
final provider = GithubAuthProvider();
try {
_user = await _auth.currentUser?.unlink(provider.providerId);
} catch (error) {
debugPrint('unlink ${provider.runtimeType} failed: $error');
return;
}
// 2. Delete Google account from firebase records, but to avoid
// **requires-recent-login** error we need to re-sign-in first;
try {
await _signOut();
await _signInWithGoogle();
await _auth.currentUser?.delete();
} catch (e) {
debugPrint('delete user failed: $e');
return;
}
// 3. sign in with Github;
try {
await _signOut();
await _signInWithGithub();
} catch (error) {
debugPrint('signInWithGithub failed: $error');
}
}
/// Unlink the Google provider from the currently signed in user.
///
/// Only exists for some unxepected cases whend Github acccount appeared to be
/// primary.
Future<void> unlinkGoogle() async {
await _unlinkGoogle();
await _user?.getIdToken(true);
notifyListeners();
}
Future<void> _unlinkGoogle() async {
final provider = GoogleAuthProvider();
try {
_user = await _auth.currentUser?.unlink(provider.providerId);
} catch (error) {
debugPrint('unlink ${provider.runtimeType} failed: $error');
return;
}
}
/// Clears the active user from the service, without calling signOut on the plugin.
///
/// This refreshes the UI of the app, while making it easy for users to re-login.
Future<void> clearUser() async {
_user = null;
notifyListeners();
}
}