Flutter Firebase Auth + Firestore Provider user document - firebase

A common scenario in nearly every single Flutter Firebase project is to use authStateChanges to determine logged in/out state by checking if the User object is null. But generally there are extra fields that are needed on a user, which come from Firestore. (I usually make a FirestoreUser model). It's generally really nice to be able to stream the current user's Firestore document and have it available throughout the app. But there's a few issues.
final user = context.read<User?>();
return StreamProvider<FirestoreUser?>(
create: (_) => UserRepository(FirebaseFirestore.instance)
.streamUser(user!.uid),
initialData: null,
child: const HomeScreen(),
);
StreamProvider's require an initialData. The end goal would be to use context.watch<FirestoreUser>() throughout the app, but since User (FirebaseAuth user) can be null, FirestoreUser (Firestore user) also has to be null.
final user = context.watch<FirestoreUser?>();
// this should technically never happen, but still need to check for it because it's impossible(?) to Provide a non-null FirestoreUser
if(user == null) return SizedBox.shrink();
...
return Text('Hello ${user.displayName}');
It would be super annoying if I had to do this on every. single. page. (This is going to be a large application)
The FirestoreUser should be available throughout the app, so it needs to be above MaterialApp. This makes the logged in/out logic complicated, since your login screen needs to be below MaterialApp.
I am also using MaterialApp.router using the Beamer package. They have a bunch of examples, but none of them are actually useful. Their FirebaseAuth example has everything in one widget and would never be a real world scenario.
Also, it's been a good while since null-safety was introduced but there are so few examples/tutorials out there that actually implement null-safety.
I'm convinced it is impossible to listen to authStateChanges to control logged in/out state and then only providing a non-null Firestore document to use throughout the application using MaterialApp.router. Is that asking too much? I feel like this would be the most common scenario. It's one I've been struggling with for my past 5+ apps.

I implemented the same scenario with MaterialApp.router, I hope my solution helps you.
I set up a ChangeNotifierProvider to handle authentication state called MyAuthState. MyAuthState is placed above MaterialApp.router, I actually use more providers, so it looks like the following. For the navigation to be accessibly (e.g. route the user to login etc.) I add the RouterDelegate to the ChangeNotifierProvider:
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => MyAuthState(
routerDelegate: ...)),
...
],
builder: (context, child) {
return MaterialApp.router(
...
);
}
Then in MyAuthState I listen to Firebase Authentication state changes, save current authentication state and start the stream to listen to my own Firestore user collection if a user is authenticated. Something like:
StreamSubscription<MyUser?>? _myUserStream;
...
FirebaseAuth.instance.authStateChanges().listen((User? firebaseUser) {
// save authentication state to know whether a user is authenticated
// if it is not null, start a stream
_myUserStream = <stream to listen snapshots for my own user collection>;
notifyListeners();
});
After this I use an authentication router as the home route of my application, consuming MyAuthState provider, and this will decide whether to show a login screen or something else (the code uses an enum for the different states):
class _MyAuthRouter extends State<MyAuthRouter> {
#override
Widget build(BuildContext context) {
return Consumer<MyAuthState>(builder: (context, myAuthState, child) {
switch (myAuthState.status) {
// before first auth state => progress indicator
case MyAuthStatus.unknown:
return const CircularProgressIndicator();
// user is logged out, not found or disabled => login screen
case MyAuthStatus.loggedOut:
case MyAuthStatus.notFound:
case MyAuthStatus.disabled:
return const MyAuthLogin();
// email not verified => verify email screen
case MyAuthStatus.emailUnverified:
return const MyAuthVerifyEmail();
// logged in => home screen
case MyAuthStatus.loggedIn:
return const MyPageHome();
}
});
}
}
And since I only get to MyPageHome upon user is logged in, I know that the stream for my user collection will be available and I can use context.read or context.watch to get the user's data.

Related

Create Firebase Dynamic Link including authentication code to reset password in-app

I'm currently working on a Flutter app where I need the user to sign in. Obviously, there might be cases where the user forgets his password, thus I need to provide a functionality to let the user reset his password.
I use Firebase as backend and sign up and sign in work as well as resetting the password using the default webview provided by Firebase out of the box. However, I'd like to provide the possibility that users of the mobile app are redirected to a custom password reset screen within the app. If I understand this correctly, this is what Dynamic Links are used for - I've also seen that they can be dynamically created from within the app.
Now, obviously, I don't want to simply redirect the user to something like https://www.myapp.com/reset-password, because then I feel that it would be hard to tell which password change belongs to which reset request. So, I thought it might be useful to integrate some kind of authentication code that is contained within the dynamic link, such that the server can identify the user for each password reset.
To accomplish this, I integrated some code that I found on this SO article and modified it a bit to generate a Dynamic Link:
Future<Uri> createDynamicLink({#required String ?mail}) async {
int randomAuthCode = Random().nextInt(1000000);
final DynamicLinkParameters parameters = DynamicLinkParameters(
uriPrefix: "https://myapp.page.link",
link: Uri.parse('https://myapp.page.link/reset-password?authcode=$randomAuthCode'),
androidParameters: AndroidParameters(
packageName: "com.myapp.client.my_app_frontend",
minimumVersion: 1
),
);
final link = await parameters.buildUrl();
final ShortDynamicLink shortenedLink = await DynamicLinkParameters.shortenUrl(
link,
DynamicLinkParametersOptions(shortDynamicLinkPathLength: ShortDynamicLinkPathLength.unguessable)
);
return shortenedLink.shortUrl;
}
However, I don't really get by now how to properly integrate this to send the email based off of this, and also, when to call that function.
The code which is triggered upon requesting the password reset email for an entered email address is the following, although I'm not sure if I need to add actionCodeSettings or not:
void _handleLookupRequest() async {
//some input validators ...
LoadingIndicatorDialog dialog = LoadingIndicatorDialog();
dialog.setContext(context);
dialog.show(context);
final FirebaseAuth auth = FirebaseAuth.instance;
await auth.sendPasswordResetEmail(
email: email,
//actionCodeSettings: //do I need to add these??
).then((user) {
dialog.dismiss();
Navigator.of(context).pop();
})
.catchError((error) {
dialog.dismiss();
String errorType = Errorparser().parseFirebaseAuthErrorType(error);
NotificationDialog().show(
context,
errorType,
Errorparser().parseFirebaseAuthErrorMessage(error)
);
}
);
}
I don't know if I'm perhaps just overengineering this because Firebase already guarantees a safe method to identify the correct user but I only started using Firebase yesterday, so I'm still getting used to all the features. Hopefully someone can help me to implement an in-app password reset like this as I had quite a hard time finding any information on this topic.

What is the correct way to use next-auth with Google OAuth and users stored in the database?

I'm new to next-auth, and I'm looking for some help.
I have added the Google OAuth provider, and now when I run signIn("google") function on the frontend, it automatically takes me to the google's login page, and logs me in, somehow, without ever touching my database.
When the google authentication is complete, I need to be able to create a new user in my database, or retrieve the existing one if they have already signed up before (because I need to store all kinds of custom information about the user, not just their email).
And I want to make user's information available on the session object from useSession()hook. Right now I'm seeing some kind of default user info (with name, email, and image field which I didn't define).
When I was using a regular Express server and Passport, the code looked kinda like this:
const googleAuth = new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/api/v1/profiles/google/callback",
},
async (req, accessToken, refreshToken, profile, done) => {
const existingProfile = await Profile.findOne({ email: profile.emails[0].value })
/* Found user, return him and move along. */
if (existingProfile) return done(null, existingProfile)
/* Haven't found profile with this googleId, create a new one. */
const newProfile = await new Profile({
googleId: profile.id,
email: profile.emails[0].value,
})
newProfile.save()
done(null, newProfile)
}
)
So I would still be creating the users in my database, and retrieving their information on log in, so that I could send it to the client.
Where does this kind of code supposed to go when I'm using the serverless next-auth?
And a second, but kind of related question - what's that default user object that gets provided to me in the session object? The one with the name, email, and image fields that next-auth seems to create for me? How can I make it use the user object I'm returning from my database instead?
(I've done my best to look through the tutorials and examples, but couldn't find one that explains this clearly.)
I don't know if you still need this, but I hope it helps someone:
Oauth kinda mixes up Sign In and Sign Up, so if you want to have Google authentication what you probably want to do is create a callback of the Sign In function in /api/auth/[...nextauth].js, then get the account and profile as parameters and access to its provider.
async signIn({account, profile}) {
if(account.provider === 'google') {
//check if user is in your database
if(user NOT in DB) {
//add your user in DB here with profile data (profile.email, profile.name)
}
return true
}
You always want to return true since you always want to log in independently if it is in your DB or not.
Regarding the session object, you can also add a callback and access to the default session (that you can modify), token and user. Here you can retrieve all information you want from your database, add it to the session object and return it.
async session({ session, token, user }) {
const newData = DB.find(...).data
session.newfield = newInfo
return session
}

How to check if a specific key exists in a specific node in Firebase real-time database with flutter?

I am trying to check if a specific key exists in a specific node in Firebase real-time database with flutter. Code:
var pSnapshot = FirebaseDatabase.instance
.reference()
.child('Parents')
.child(currentUId)
.key;
if (pSnapshot == currentUId) {
print(pSnapshot);
print(currentUId);
print("Key & UID Match in Parents");
// redirect to Parents
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
ParentProfile(),
),
);
}
I stored each user under a key which is also their user id when they register.
What I'm trying to do is to say if the current logged in user's id exists as a key in 'Parents', then that means that user is a parent, and therefore I will redirect them to parent page.
This doesn't work, any idea how to deal with this situation?
Your code is not reading anything from the database yet.
The only way to check if a value exists at a path, is to read the data at that path from the database (for example, by calling once() and then checking if the snapshot has a value.
From an existing project of mine:
var snapshot = await _db.child("accessTokens").child(token).once();
if (snapshot.value != null) {
...
}
I was facing the same issue and got aroud it, this was my resolution,
I was trying to store extra user info whenever a user signs in, but then i needed to check if the user id exists and then if the user exists then move to homepage, if the user doesnt exist then create the user info, this is the code doing the checks
Future getUserInRtdb() async {
var snapshot =
await rootRef.child(FirebaseAuth.instance.currentUser!.uid).once();
print(snapshot.snapshot.exists);
// this print statement returns false if the child is not found
// returns true if the child is found
}
using this in a future builder does the checks pretty well

How to Update data for FirebaseUser Stream when Email is Verified (Flutter)

I'm building a flutter app with firebase auth dealing with users. My goal is to streamline the user onboarding experience by allowing email sign up and verification without forcing the user to sign out and sign in again to use the full features. Before testing with real users, this is exactly what I had configured but the overwhelming feedback is that it interrupts and may even cause the user to abandon the authentication flow altogether; the app should simply detect this change in verification status without having to log out.
WHAT I'VE TRIED
I'm using the following in main.dart to provide auth state throughout the app:
StreamProvider < FirebaseUser > .value(
value: FirebaseAuth.instance.onAuthStateChanged,
),
I extract it with
user1 = Provider.of<FirebaseUser>(context, listen: true);
in the individual pages and access different properties like user1.isEmailVerified in ternary expressions to determine what is displayed to the user.
I know that onAuthStateChanged doesn't get called for changes in email verification, only for user sign in and sign out as explained in the accepted answer for this question, but I still need to update the user value that determines the UI shown when a user verifies their email. I have chosen to use a button "complete email verification" which will manually trigger the check and reload of the user profile with the new value once verified. This is the code for that button:
() async {
// 1
FirebaseUser user =
await _firebaseAuth.currentUser();
print(user.isEmailVerified); // returns false
// 2
await user.getIdToken();
print(user.isEmailVerified);// returns false
// 3
await user.reload();
print(user.isEmailVerified);// returns false
//4
user = await _firebaseAuth.currentUser();
print(user.isEmailVerified);// this finally returns true
}
where final _firebaseAuth = FirebaseAuth.instance;
I've noticed that calling .currentUser() twice (before and after reload()) is the only way to actually return true for the .isEmailVerified property without logging out and back in. This is an answer I found in this GitHub thread.
If I press the button again, all the print statements above return true.
THE PROBLEM
That's great, but the problem is the user1 variable that stores the FirebaseUser stream does not change or update even after this, so the UI remains the same despite the user email being verified. I know this because I've placed the following checks in the code:
print('this one is from the stream:');
print(user1.isEmailVerified);
It still returns false even when in the button function body, the user.isEmailVerified returns true.
I've tried calling set state within the button like this:
setState(() {user1 = user; });
But it seems that the value of user1 still remains the same and still returns false for the user1.isEmailVerified, even though I'm trying to set it to the new value derived from the button's function!
This is really frustrating as I feel I'm very close but I just don't know how to refresh the value of the user1 (firebase user stream) throughout the app or even just on that one screen. This should be more straightforward.
I've noticed that pressing hot reload after verifying the email and pressing the button above will automatically show the updated .isEmailVerified value throughout the app while debugging on flutter. Is there a way to programmatically trigger the same kind of rebuild that hot reload uses that will also refresh the firebase user stream data WITHOUT having to log out and sign back in? Maybe this is where the answer lies, but I don't know how to proceed with that.
Any help would be appreciated.
Instead of onAuthStateChanged, you can use userChanges stream which
provides events on all user changes, such as when credentials are linked, unlinked and when updates to the user profile are made. The purpose of this Stream is to for listening to realtime updates to the user without manually having to call [reload]
see https://firebase.flutter.dev/docs/auth/usage/#authentication-state
I was also facing this problem and get fixed with the help of:
await user.reload();
user = await _auth.currentUser();
user.isEmailVerified => now it is true
I came across your question while trying to figure out why user.reload() wasn't updating the email verification status of my user. Thank you for suggesting getting currentUser again, which works.
To answer your question, I use a BehaviorSubject from rxdart to store my FirebaseUser, so I can get notifications of the user changing from anywhere in the app with a StreamBuilder:
final BehaviorSubject<FirebaseUser> firebaseUser = new BehaviorSubject<FirebaseUser>();
userListener = FirebaseAuth.instance.onAuthStateChanged.listen((user) {
if (user == null) {
userSignedOut();
}
else if (currentFirebaseUser == null || user.uid != currentFirebaseUser.uid) {
processNewUser(user);
}
firebaseUser.add(user);
});
Auth state change listener didn't work for me. Field isEmailVerified remains false even after user verifies his email.
My workaround:
Started from the assumption that user leaves the app to verify his email (which mean app is paused), and he returns to the app after verifying it (app resumes).
What I did was attach a WidgetsBinding to a relevant stateful widget where I wanted to display if email was verified (but can be done elsewhere). This involves two steps.
First step is to attach the binding:
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Second step is to override the didChangeAppLifecycleState to reload the user. I created a function that does the reload and sets a new firebaseUser object
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed && !firebaseUser.isEmailVerified)
refreshFirebaseUser().then((value) => setState(() {}));
super.didChangeAppLifecycleState(state);
}
Future<void> refreshFirebaseUser() async {
await firebaseUser.reload();
firebaseUser = FirebaseAuth.instance.currentUser;
}
So what this is basically doing is to reload firebase user object everytime the user returns to the app, while its email is not verified. This seems cleaner than the best workaround that I've found in SO which consisted in setting and cancelling a recurrent action through a timer.
The FirebaseAuth Flutter plugin provides a Stream with
onAuthStateChanged, which is useful for getting updates when a user
signs-in or signs-out, but it fails to provide an update when the user
data itself changes. A common problem is when we want to detect if the
user has just verified his e-mail address, in other words get if
FirebaseUser.isEmailVerified changed its value, as this is updated
server-side and not pushed to the app.
FirebaseAuth.currentUser() will only detect changes made locally to
the user, but if any changes are made server-side, it won't detect,
unless FirebaseUser.reload() is called first and then we need to call
FirebaseAuth.currentUser() again to get the reloaded user, but still
this user won't be emitted by onAuthStateChanged.
This can help you:
https://pub.dev/packages/firebase_user_stream
You will find a manual solution or you can use the package.

Flutter - How to add Firebase-Auth user credentials to new records (FireStore documents)?

I'm trying to create a simple Crud app with Flutter and Firebase which the records (documents created in FireStore) are related to the user who has been Authenticated. Therefore the Crud functions will only be performed by the user who created the record. IE a user will only be able able to edit/update/delete the records they added in the first place.
I have the firebase_auth and crud functions working nicely with firestore. the issues i'm have is with relating the two. I have chosen to use the users email and the unique identifier (i'm not sure if it's better to use the auto generated user id or not). I have created a separate function for simply returning the current user's email as it's being added to the firestore document. The problem is the first time i add a record the user email returns null, If i submit the form again it starts working fine.
String _userEmail;
_getUserAuthEmail() {
FirebaseAuth.instance.currentUser().then((user){
setState((){this._userEmail = user.email;});
});
return this._userEmail;
}
Which is being called from the onPressed event
onPressed: () {
crudObj.addData({
'itemName': this.itemName,
'userEmail': _getUserAuthEmail(),
}).then((result) {
dialogTrigger(context);
}).catchError((e) {
print(e);
});
},
As i'm just starting out please let me know if there is a better approach. Cheers.
You are getting null because you are not waiting for the currentUser method to settle. Change the _getUserEmail method like this:
String _userEmail;
_getUserAuthEmail() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
setState(() {
_userEmail = user.email;
});
return this._userEmail;
}
Also, about this
"I have chosen to use the users email and the unique identifier (i'm not sure if it's better to use the auto generated user id or not)."
I suggest you using the user's uid for saving user related stuff.

Resources