Flutter / Firebase google sign in skips normal flow - firebase

I'm having some problems with google sign in. It works - it signs me in but the screen just kind of flashes and the sign in flow where you'd choose your account never appears, then it just signs me in with my main google account.
Not sure what I'm doing wrong.
Its not normal behavior. I think normally a little dialog pops up and you get the chance to choose what google account you want to sign in with. This is the part that is not happening. I even tried on my phone, with a never installed before apk and never saw that dialog but it signed me with the default account.
Any help greatly appreciated:
general_providers.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:riverpod/riverpod.dart';
final firebaseAuthProvider =
Provider<FirebaseAuth>((ref) => FirebaseAuth.instance);
final authStateChangesProvider = StreamProvider<User?>(
(ref) => ref.watch(firebaseAuthProvider).authStateChanges());
auth_widget.dart. -> this one is meant to just display the home page or login page depending on auth state:
class AuthWidget extends ConsumerWidget {
const AuthWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final authStateChanges = ref.watch(authStateChangesProvider);
return authStateChanges.when(
data: (user) => user != null ? const HomePage() : LoginPage(),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, __) => Scaffold(
body: Center(
child: Text(error.toString()),
),
),
);
}
}
firebase_auth_service.dart
final authServiceProvider =
Provider<FirebaseAuthService>((ref) => FirebaseAuthService(ref.read));
class FirebaseAuthService {
FirebaseAuthService(this._read);
final Reader _read;
//TODO add in try / catch with firebase exeptions
Future<void> signInAnonymously() async {
await _read(firebaseAuthProvider).signInAnonymously();
}
User? getCurrentUser() => _read(firebaseAuthProvider).currentUser;
Future<void> signOut() async {
await _read(firebaseAuthProvider).signOut();
}
Future<UserCredential> signInWithGoogle() async {
try {
// await _googleSignIn.signIn();
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
// Obtain the auth details from the request
final GoogleSignInAuthentication? googleAuth =
await googleUser?.authentication;
// Create a new credential
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken,
idToken: googleAuth?.idToken,
);
return await _read(firebaseAuthProvider).signInWithCredential(credential);
} catch (error) {
throw FirebaseAuthException(code: error.toString());
}
}
}
The signInWithGoogle method above is pretty much copy paste from the docs except that I link my firebaseAuthProvider in the final line to get that top level auth instance.
When I run this method from the signin page it does sign me in but just skips the normal flow.
Any ideas?

I think this is the expected behavior. You would need to log out before you get to select account again.

Related

Google Sign In current user return null Flutter

I want to integrate my app with Calendar API from Google. And in order to use it, I have to have an AuthClient (which is obtained from _googleSignIn.authenticatedClient();). The problem is, my GoogleSignIn().currentUser always return null and I don't know why. I already use Firebase Auth and Google Sign In.
This is my signInWithGoogle method:
Future signInWithGoogle() async {
try {
await GoogleSignIn().disconnect();
await FirebaseAuth.instance.signOut();
} catch (e) {
print(e.toString());
}
// Trigger the authentication flow
final GoogleSignInAccount? googleUser = await GoogleSignIn(scopes: [CalendarApi.calendarScope]).signIn();
// Obtain the auth details from the request
final GoogleSignInAuthentication googleAuth =
await googleUser!.authentication;
// Create a new credential
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// Once signed in, return the UserCredential
UserCredential result =
await FirebaseAuth.instance.signInWithCredential(credential);
User user = result.user!;
// note: this line always return null and I don't know why
print('current user auth ${GoogleSignIn().currentUser.toString()}');
return _userFromFirebaseUser(user);
}
Did I do something wrong in my code? Any help will be appreciated, thank you!
I also had the issue of GoogleSignIn().currentUser always being null but managed to (finally!) fix it by only initialising GoogleSignIn() once.
For those who want more details: I did this by creating a class called AuthManager that handles everything authentication-related, and making GoogleSignIn one of the parameters required to initialise it (since I'm using Firebase, this was the other parameter):
class AuthManager {
final FirebaseAuth _auth;
final GoogleSignIn _googleSignIn;
AuthManager(this._auth, this._googleSignIn);
Future signInWithGoogle() async {
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
// etc....
}
GoogleSignInAccount? get googleAccount {
return _googleSignIn.currentUser;
}
}
And I initiaised by AuthManager class ONCE at the top of my app in a Provider, meaning that I can access it anywhere in my app.
In main.dart:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// To use AuthManager throughout app without initialising it each time
Provider<AuthManager>(
create: (_) => AuthManager(
FirebaseAuth.instance,
GoogleSignIn(scopes:
// Put whatever scopes you need here
),
),
),
// etc...
(Note: I used MultiProvider as I had other things I wanted to put, but if you only have one, you can obviously just go straight to Provider).
Now I can successfully get the current google user by getting googleAccount through my AuthManager class.

Firebase Authentication in Flutter Check if user is a new user

I have implemented the following login method and I am trying to use the isNewUser function to push a new screen:
Future<void> googleLogin() async {
try {
final googleUser = await GoogleSignIn().signIn();
if (googleUser == null) return;
final googleAuth = await googleUser.authentication;
final authCredential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
UserCredential userCredential =
await FirebaseAuth.instance.signInWithCredential(authCredential);
if (userCredential.additionalUserInfo!.isNewUser) {
return const SignUpNewUser();
}
} on FirebaseAuthException catch (e) {
AlertDialog(
title: const Text("Error"),
content: Text('Failed to sign in with Google: ${e.message}'),
);
}
}
I get the following error:
A value of type 'SignUpNewUser' can't be returned from the method 'googleLogin' because it has a return type of 'Future<void>'.
I'm pretty sure that I placed it in the correct spot to implement the function, but I have no idea how to do it in a Future.
The problem is in the return type, you need change the type from void to dynamic.
Future<dynamic> googleLogin() async {...}
you can return a widget directly but that doesn't makes sense so you need to use Navigator in order to push to a new screen.
Add context as parameter in the method googleLogin()
Use this Navigator.push(context,MaterialPageRoute(builder: (context) =>your_new_screen()),);
in the condition userCredential.additionalUserInfo!.isNewUser
In the above replace your_new_screen() with the widget you have returned before ie. SignUpNewUser()

How to navigate on the another page after google sign in

Future<void> signUpWithGoogle() async {
try {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final AuthCredential credential = GoogleAuthProvider.getCredential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
final FirebaseUser user =
(await _auth.signInWithCredential(credential)).user;
return user;
} catch (error) {
print(error);
}
}
I have done that above code to sign in with google and called the function of google sign button like this
signUpWithGoogle().then((value) => Navigator.of(context).push(MaterialPageRoute(builder: (_) {
return HomePage();
})));
But on first time when apk is installed normally the app is asking for choosing the google account
But after log out when i tap on the google SignIn button it is not asking in pop up menu to select the account.
And one more problem is their on clicking on the google signin button firsts it goes to the HomePage() then signIn is hapenning.
#override
void initState() {
super.initState();
getCurrentUser();
}
Future<void> getCurrentUser() async {
FirebaseUser user = await _auth.currentUser();
bool result = await facebookSignIn.isLoggedIn;
if (user != null && user.isEmailVerified == true) {
print("Email");
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) {
return HomePage();
}));
}
}
Is this is the correct method to navigate to the HomePage() for those users who is signed in when app starts
Whenever user logout and sign in again with google account android is smart enough to provide google account directly to the app without giving sign in pop up
for your second problem plz ref this answer -:
Why might a function not get called in initState(){ super.initState()} in flutter, but work perfectly fine if called later in the same page?
first of all i am also new to flutter so this might not be the best solution however this is what i implement in my app for the authentication part
first i created a User class that contain an ID for the user
then i created a stream of user to my app so the app will always be provided with this value and what ever change happen to it in my services this is the code
final FirebaseAuth _auth = FirebaseAuth.instance;
Stream<FirebaseUser> user; // firebase user
User _userFromFireBaseUser(FirebaseUser user) {
return user != null ? User(uid: user.uid) : null;
}
// //auth change user stream
Stream<User> get userStream {
return _auth.onAuthStateChanged.map(_userFromFireBaseUser);
// or we can use .map((FirebaseUser user) => _userFromFireBaseUser(user) );
}
in my main widget
StreamProvider<User>.value(
lazy: false,
value: AuthService().userStream,
child: MaterialApp()//your main widget
then i created a statless wrapper class that read the stream value and according move to a page, in my app i used the wrapper to go to the sign in if the user is null else go to the profile page, in your application i guess it will go to the homepage
class ProfileWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
//print(user.uid);
if (user == null) {
print('no user');
return SignUpPage();
} else if (user != null) {
print('there is user');
print(user.uid);
return ProfilePage();
// print('there is user' + user.displayname);
// print('there is user' + user.photourl);
}
}
}
also you need to add the provider package in your pubspec.yaml file
provider: ^4.1.1
by doing this you don't need to handle any navigation between the home and the sign up, if your user is signed in you will automatically be navigated to the home page.
another solution if that is not what you are looking for, after the google sign function finishes check if the firebase user is not null, if there is a user navigate to your homepage

How to Sign In with Twitter using Firebase_Auth with Flutter

How should one program a Sign In with Twitter feature using firebase_auth and Flutter?
I see a few examples using flutter_twitter_login or flutter_twitter, however they use a now Deprecated API and folks complain about Apple Store Rejection.
Firebase Auth offers a TwitterAuthProvider, but the following code remains incomplete:
final AuthCredential credential = TwitterAuthProvider.getCredential(
authToken: twitterAccessToken,
authTokenSecret: twitterAccessTokenSecret,
);
final AuthResult result = await auth.signInWithCredential(credential);
I was able to solve this using 3 resources:
The Flutter Facebook Sign In (with Firebase) in 2020 article
The Log in with Twitter guide
The Dart OAuth1 library
Ultimately, I was able to completely remove the flutter_twitter package, yet still support Sign in with Twitter.
Similar to the CustomWebView outlined in the Facebook solution, I created a TwitterLoginScreen like:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:oauth1/oauth1.dart';
/// Twitter Login Screen.
/// See [Log in with Twitter](https://developer.twitter.com/en/docs/basics/authentication/guides/log-in-with-twitter).
class TwitterLoginScreen extends StatefulWidget {
final twitterPlatform = Platform(
'https://api.twitter.com/oauth/request_token', // temporary credentials request
'https://api.twitter.com/oauth/authorize', // resource owner authorization
'https://api.twitter.com/oauth/access_token', // token credentials request
SignatureMethods.hmacSha1, // signature method
);
final ClientCredentials clientCredentials;
final String oauthCallbackHandler;
TwitterLoginScreen({
#required final String consumerKey,
#required final String consumerSecret,
#required this.oauthCallbackHandler,
}) : clientCredentials = ClientCredentials(consumerKey, consumerSecret);
#override
_TwitterLoginScreenState createState() => _TwitterLoginScreenState();
}
class _TwitterLoginScreenState extends State<TwitterLoginScreen> {
final flutterWebviewPlugin = FlutterWebviewPlugin();
Authorization _oauth;
#override
void initState() {
super.initState();
// Initialize Twitter OAuth
_oauth = Authorization(widget.clientCredentials, widget.twitterPlatform);
flutterWebviewPlugin.onUrlChanged.listen((url) {
// Look for Step 2 callback so that we can move to Step 3.
if (url.startsWith(widget.oauthCallbackHandler)) {
final queryParameters = Uri.parse(url).queryParameters;
final oauthToken = queryParameters['oauth_token'];
final oauthVerifier = queryParameters['oauth_verifier'];
if (null != oauthToken && null != oauthVerifier) {
_twitterLogInFinish(oauthToken, oauthVerifier);
}
}
});
_twitterLogInStart();
}
#override
void dispose() {
flutterWebviewPlugin.dispose();
super.dispose();
}
Future<void> _twitterLogInStart() async {
assert(null != _oauth);
// Step 1 - Request Token
final requestTokenResponse =
await _oauth.requestTemporaryCredentials(widget.oauthCallbackHandler);
// Step 2 - Redirect to Authorization Page
final authorizationPage = _oauth.getResourceOwnerAuthorizationURI(
requestTokenResponse.credentials.token);
flutterWebviewPlugin.launch(authorizationPage);
}
Future<void> _twitterLogInFinish(
String oauthToken, String oauthVerifier) async {
// Step 3 - Request Access Token
final tokenCredentialsResponse = await _oauth.requestTokenCredentials(
Credentials(oauthToken, ''), oauthVerifier);
final result = TwitterAuthProvider.getCredential(
authToken: tokenCredentialsResponse.credentials.token,
authTokenSecret: tokenCredentialsResponse.credentials.tokenSecret,
);
Navigator.pop(context, result);
}
#override
Widget build(BuildContext context) {
return WebviewScaffold(
appBar: AppBar(title: Text("Twitter Login")),
url: "https://twitter.com",
);
}
}
Then, the AuthCredential result from this screen can be passed to FirebaseAuth.signInWithCredential.
To sign in with Twitter do the following:
Future<FirebaseUser> loginWithTwitter() async {
var twitterLogin = new TwitterLogin(
consumerKey: 'key',
consumerSecret: 'secretkey',
);
final TwitterLoginResult result = await twitterLogin.authorize();
switch (result.status) {
case TwitterLoginStatus.loggedIn:
var session=result.session;
final AuthCredential credential= TwitterAuthProvider.getCredential(
authToken: session.token,
authTokenSecret: session.secret
);
FirebaseUser firebaseUser=(await firebaseAuth.signInWithCredential(credential)).user;
print("twitter sign in"+firebaseUser.toString());
break;
case TwitterLoginStatus.cancelledByUser:
break;
case TwitterLoginStatus.error:
break;
}
Use twitterlogin and pass the consumer key and consumer secret key, then use the method getCredential() and signInWithCredential to log in.
They have shared a common sample in the home page itself, only the 'sign in provider' changes, rest is same for all (google, fb and twitter). the result has a user property which will return the user details, check with the below code
final AuthCredential credential = TwitterAuthProvider.getCredential(
authToken: twitterAccessToken,
authTokenSecret: twitterAccessTokenSecret,
);
final AuthResult result = await auth.signInWithCredential(credential);
final FirebaseUser user = result.user;
print("signed in " + user.displayName);
This worked for me. (Referred from https://firebase.flutter.dev/docs/auth/social#twitter)
import 'package:twitter_login/twitter_login.dart';
Future<UserCredential> signInWithTwitter() async {
// Create a TwitterLogin instance
final twitterLogin = new TwitterLogin(
apiKey: '<your consumer key>',
apiSecretKey:' <your consumer secret>',
redirectURI: '<your_scheme>://'
);
// Trigger the sign-in flow
final authResult = await twitterLogin.login();
// Create a credential from the access token
final twitterAuthCredential = TwitterAuthProvider.credential(
accessToken: authResult.authToken!,
secret: authResult.authTokenSecret!,
);
// Once signed in, return the UserCredential
return await FirebaseAuth.instance.signInWithCredential(twitterAuthCredential);
}
This wasn't working initially for me.
Things I had to change in order for this to work.
Request for Elevated Permissions in Twitter Developer Portal.
Create a custom scheme (callback URL) and configure the ios/android files accordingly. As given in (https://pub.dev/packages/twitter_login).
Also configure this callback URL in the Twitter dev portal.

Returning null user data from Firestore. How to reference it globaly instead?

I'm quite new to Flutter and I've been struggling to access a user's document on Firestore.
On the profile page,
I'm setting the current user's UID inside initState, but uid returns null for a quick second, then the page updates with correct info.
So I am able to retrieve a certain field (like displayName), but it isn't quite the best practice. I don't want to have a bunch of boilerplate code and await functions mixed with UI and such.
Code:
FirebaseUser user;
String error;
void setUser(FirebaseUser user) {
setState(() {
this.user = user;
this.error = null;
});
}
void setError(e) {
setState(() {
this.user = null;
this.error = e.toString();
});
}
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then(setUser).catchError(setError);
}
Then in my body I have a Stream builder to get the document.
body: StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(user.uid)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.deepOrange),
),
);
} else {
var userDocument = snapshot.data;
return showProfileHeader(userDocument);
}
},
)
I want to make 'global' references to be accessed throughout the app. Instead of getting the user's id on every page and streaming a specific field when I might need multiple ones.
The only ways I found online to do something similar, created lists with all the data in it. I feel like this might get extra fields I don't need.
How can I make data from Firestore available across the app?
I am using the "Provider" package for doing state management across my app. Nowadays its also the suggested way by the google flutter team when it comes to state management. See the package here: https://pub.dev/packages/provider
Regarding Firebase Auth and accessing the credentials application wide, i am using that said package like stated on this page:
https://fireship.io/lessons/advanced-flutter-firebase/
Short version below. Bootstrap your app like so:
import 'package:provider/provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Make user stream available
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged),
// not needed for your problem but here you can see how
// to define other Providers (types) for your app.
// You need a counter class which holds your model of course.
ChangeNotifierProvider(builder: (_) => Counter(0)),
],
// All data will be available in this child and descendents
child: MaterialApp(...)
);
}
}
Then in your child widgets, just do:
// Some widget deeply nested in the widget tree...
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<FirebaseUser>(context);
return Text(user.displayName) // or user.uid or user.email....
}
}
This should do the trick.
That happens because FirebaseAuth.instance.currentUser() returns a future, and until that future is completed, you will not have the proper FirebaseUser object.
Making the user object global is not a bad idea. In addition, you can hook it up to the FirebaseAuth stream so that it gets updated everytime the user auth status changes, like so in a user.dart file:
class User {
static FirebaseUser _user;
static get user => _user;
static void init() async {
_user = await FirebaseAuth.instance.currentUser();
FirebaseAuth.instance.onAuthStateChanged.listen((firebaseUser) {
_user = firebaseUser;
});
}
}
You can call User.init() in main() and access the user object with User.user.

Resources