Flutterfire GetX AuthFlow - flutter

I am using flutter, firebase auth, and getx to build an app. After checking if the user exists, the page should redirect normally. But it is not. What am I doing wrong? I have been stuck on this for some time now but I cannot figure it out. It is stuck on the loading screen especially when the user is null. If I create a user in firebase, it redirects normally.
class AuthController extends GetxController {
static AuthController authInstance = Get.find();
final phone = ''.obs;
final phoneOtp = ''.obs;
final verificationID = ''.obs;
final FirebaseAuth auth = FirebaseAuth.instance;
late Rx<User?> firebaseUser;
#override
void onReady() {
super.onReady();
firebaseUser = Rx<User?>(auth.currentUser);
firebaseUser.bindStream(auth.userChanges());
ever(firebaseUser, _setInitialScreen);
}
_setInitialScreen(User? user) {
if (user == null) {
Get.offAllNamed("/onboarding");
} else {
Get.offAllNamed("/landing");
}
}
}

Firebase has a function to check the user's status, use it instead it's easier and more efficient. Use a streambuilder
StreamBuilder(
stream: AuthServices().onChangedUser,
builder: (context, snapshot) {
return snapshot.data == null ? Get.offAllNamed("/onboarding"); : Get.offAllNamed("/landing");;
},
),

Related

Null check operator used on a null value problem, I am very confused

I am new in flutter app.
I have made a subcollection products in users collections. It will show to all when a user will log in to their account. When the user clicks on the My Products button it will only show those products which are created by the login user. I user stream builder and use this FirebaseFirestore.instance
.collection('users')
.doc(LoginUser!.uid)
.collection('products')
.snapshots() , to get the data.
But when I click on the button it throws an exception. Which provide on the screen shots.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class UserProductList extends StatefulWidget {
UserProductList({Key? key}) : super(key: key);
#override
_UserProductListState createState() => _UserProductListState();
}
class _UserProductListState extends State<UserProductList> {
User? LoginUser;
#override
void initState() {
super.initState();
getCurrentUser();
}
void getCurrentUser() async{
var LoginUser=await FirebaseAuth.instance.currentUser;
print(LoginUser!.email);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('users')
.doc(LoginUser!.uid)
.collection('products')
.snapshots() ,
builder:(BuildContext, AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot){
return ListView(
children: snapshot.data!.docs.map((document){
return ElevatedButton(onPressed: getCurrentUser, child: Text('data'));
}).toList(),
);
},
),
);
}
}
FIrst of all, FirebaseAuth.instance.currentUser is not a Future it doesn't need to be awaited. You can use it straight away in your StreamBuilder
.doc(FirebaseAuth.instance.currentUser?.uid ?? '')
My mistake was by making the currentUser future by using async and await. that's why steamBulder did not get the user id to fetch the data and throwing error for null user.
void getCurrentUser() async{
var LoginUser=await FirebaseAuth.instance.currentUser;
print(LoginUser!.email);
}```
So, I just remove this portion code and instead of that I just use this **var LoginUser = FirebaseAuth.instance.currentUser;** to get my **login user Uid** and it's working perfectly

Retrigger FutureBuilder on Login / Registration

I'm currently stuck at what I hope is a simple problem, I just tried so much that I probably just can't see the solution any longer.
I have a Landing Page that checks via a future whether the user has an active session or not (Parse Backend).
I manage to make successful login and registration requests, just the screen doesn't change, meaning the future builder doesn't rebuild. When I hot reload everything works fine, but I don't manage to automatically trigger the hot reload. I user Riverpod for state management.
The hasUserLogged() Method is supplied via Riverpod by an AuthBase class.
I hand over the updatedUser method to the AuthScreen to trigger it on login/signUp, but it doesn't trigger a rebuild of the FutureBuilder.
I thought getting an updatedUser from Server would also supply me in the next step with information whether the user has its email verified, but that's the follow up problem (but I would appreciate a pointer in the right direction how to solve the 4x4 user matrix: has token / no token & verified / unverified e-mail and redirecting to Auth / Verify E-Mail / HomePage depending on combinations..)
Anyhow, for now - how can I trigger the rebuild of the FutureBuilder upon Login/SignUp Button press in the AuthScreen?
class LandingPage2 extends StatefulWidget {
#override
_LandingPage2State createState() => _LandingPage2State();
}
class _LandingPage2State extends State<LandingPage2> {
Future<ParseUser> _updateUser() async {
final auth = context.read(authProvider);
ParseUser currentUser = await ParseUser.currentUser() as ParseUser;
if (currentUser != null) {
ParseResponse update = await currentUser.getUpdatedUser();
if (update.success) {
currentUser = update.result as ParseUser;
await auth.hasUserLogged();
setState(() {
return currentUser;
});
}
}
if (currentUser == null) {
print('null User');
}
}
/// Check if user session token is valid
Future<bool> hasUserLogged() async {
ParseUser currentUser = await ParseUser.currentUser() as ParseUser;
// return false if no user is logged in
if (currentUser == null) {
return false;
}
//Validates that the user's session token is valid
final ParseResponse parseResponse =
await ParseUser.getCurrentUserFromServer(
currentUser.get<String>('sessionToken'));
if (!parseResponse.success) {
print('invalid session. logout');
//Invalid session. Logout
await currentUser.logout();
return false;
} else {
print('login successfull');
return true;
}
}
#override
Widget build(BuildContext context) {
final auth = context.read(authProvider);
return FutureBuilder<bool>(
future: auth.hasUserLogged(),
builder: (context, snapshot) {
print('futurebuilder rebuild');
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return SplashScreen();
break;
default:
if (snapshot.hasData && snapshot.data) {
return HomePage();
} else {
return AuthScreen(_updateUser);
}
}
},
);
}
}
Any help is highly appreciated, struggle since hours and my head can't wrap around why it is not working :-/
Thank you #Randal Schwartz, 'watch' made it happen, after I created an AuthNotifier and StateNotifierProvider to manage the user state and depending on that user in the hasUserLogged() method.
If anyone is also struggling - that's the working version:
import 'dart:async';
import 'package:app/screens/splash_screen.dart';
import 'package:app/services/top_level_providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk.dart';
import 'auth_screen.dart';
import 'home.dart';
class LandingPage2 extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
final auth = watch(authProvider);
/// Check if user session token is valid
Future<bool> hasUserLogged() async {
print('hasUserLogged - Fired');
/// watch current User state -- Triggers rebuild after login!
final authNotifier = watch(authNotifierProvider.state);
final ParseUser currentUser = authNotifier;
// return false if no user is logged in
if (currentUser == null) {
print('currentUserNULL');
return false;
}
//Validates that the user's session token is valid
final ParseResponse parseResponse =
await ParseUser.getCurrentUserFromServer(
currentUser.get<String>('sessionToken'));
if (!parseResponse.success) {
print('invalid session. logout');
//Invalid session. Logout
await currentUser.logout();
return false;
} else {
print('login successfull');
return true;
}
}
return FutureBuilder<bool>(
future: hasUserLogged(),
builder: (context, snapshot) {
print('futurebuilder rebuild');
// print(snapshot.data);
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return SplashScreen();
break;
default:
if (snapshot.hasData && snapshot.data) {
/// Add Verify E-Mail Logic here - Another Future Builder??
return HomePage();
} else {
// _updateUser();
return AuthScreen();
}
break;
}
},
);
}
}
The Auth Notifier:
/// Auth Notifier Class
class AuthNotifier extends StateNotifier<ParseUser> {
AuthNotifier(ParseUser state) : super(state);
setCurrentUser(ParseUser user) {
state = user;
}
void clearUser() {
state = null;
}
}
And the provider:
final authNotifierProvider = StateNotifierProvider((ref) {
return AuthNotifier(null);
});
This is triggered after the active User after login / registration is received and thus triggers rebuild of hasUserLogged.
authNotifier.setCurrentUser(user);
Appreciate the help! Did cost me a lot of time... Having to switch away from firebase sucks...

I have a question about navigating to the next page conditionally in initstate

I want to implement Auto Login with Shared preferences.
What I want to implement is that as soon as 'LoginPage' starts, it goes to the next page without rendering LoginPage according to the Flag value stored in Shared preferences.
However, there is a problem in not becoming Navigate even though implementing these functions and calling them from initstate. What is the problem?
//Login Page
void autoLogIn() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String userId = prefs.getString('username');
print("ddddddddddddddd");
SocketProvider provider = Provider.of<SocketProvider>(context);
Future.delayed(Duration(milliseconds: 100)).then((_) {**//I tried giving Delay but it still didn't work.**
Navigator.of(context).pushNamedAndRemoveUntil("/MainPage", (route) => false);
});
}
#override
void initState() {
// TODO: implement initState
loginBloc = BlocProvider.of<LoginBloc>(context);
if(!kReleaseMode){
_idController.text = "TESTTEST";
_passwordController.text = "1234123";
}
initBadgeList();
autoLogIn();**//This is the function in question.**
super.initState();
print("1111111111111111");
}
I don't think you should show LoginPage widget if user is already logged in and then navigate to main page.
I suggest you to use FutureBuilder and show either splash screen or loader while performing await SharedPreferences.getInstance(). In this case your App widget should look like this:
class App extends MaterialApp {
App()
: super(
title: 'MyApp',
...
home: FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.data != null) {
final SharedPreferences prefs = snapshot.data;
final userId = prefs.getString('username');
...
return userId == null ?? LoginPage() : MainPage();
} else {
return SplashScreenOrLoader();
}
}));
}
But if you still want to show LoginPage first, just replace SplashScreenOrLoader() with LoginPage() in code above.

Dart/Flutter: How to return widget from async function?

I have a simple app from which a user can login and on logging in, a token is generated and stored on the device.
When the app starts, the following code below runs.
import 'package:coolkicks/screens/authpage.dart';
import 'package:coolkicks/screens/homescreen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:logger/logger.dart';
class Authenticate extends StatefulWidget {
#override
_AuthenticateState createState() => _AuthenticateState();
}
class _AuthenticateState extends State<Authenticate> {
final storage = new FlutterSecureStorage();
var log = Logger();
bool authenticated = false;
void checkToken() async {
String token = await storage.read(key: 'token');
if (token == null || token.length == 0) {
authenticated = false;
} else {
authenticated = true;
print(token);
log.d(token);
log.i(token);
}
}
#override
Widget build(BuildContext context) {
//check if Authenticated or Not
//return either Products Home Screen or Authentication Page
//If token exists, return Home screen
//Else return authpage
checkToken();
if(authenticated) {
return HomeScreen();
}
else {
return AuthPage();
}
}
}
My issue is that retrieving the token returns a future and takes some time to execute.
So it always returns the default authenticated = false
You should use FutureBuilder
FutureBuilder<String>(
future: storage.read(key: 'token'),
builder: (context, snapshot) {
if (snapshot.hasData) {
final token = snapshot.data;
if (token == null || token.length == 0) {
return HomeScreen();
} else {
return AuthPage();
}
}
if (snapshot.hasError) return WidgetThatShowsError();
// by default show progress because operation is async and we need to wait for result
return CircularProgressIndicator();
},
);
Don't do this. build should be idempotent. You should call checkToken() in initState. Then you can either use setState or you use a FutureBuilder.
But, provided the naming, you should rather just provide a splash screen, check the condition and navigate to either screen, instead of using 1 route for both screens.

Implement realtime online/offline status with flutter and firebase

what is the best way to show in an app if the user is online or offline?
Frontend -> Flutter
Backend -> Firestore Cloud and Firebase Auth.
I have a collection of users in Firestore that contains documents. Each document is a user and contain status field. In Flutter, I can update this field every time that user log in or log out but if you close the app it is not updated.
You can extend your statefulWidget State class with WidgetsBindingObserver
like
class _HomePageState extends State<HomePage>
with WidgetsBindingObserver
and initState method add WidgetsBinding.instance.addObserver(this);.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
Later overide didChangeAppLifecycleState method
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed)
//TODO: set status to online here in firestore
else
//TODO: set status to offline here in firestore
}
IT CAN DONE BY WidgetsBindingObserver();
class _HomeState extends State<Home> with WidgetsBindingObserver {...}
initialize it first
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
}
After add this function to listen for app state didChangeAppLifecycleState()
String? changes;
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
final isBg = state == AppLifecycleState.paused;
final isClosed = state == AppLifecycleState.detached;
final isScreen = state == AppLifecycleState.resumed;
isBg || isScreen == true || isClosed == false
? setState(() {
// SET ONLINE
})
: setState(() {
//SET OFFLINE
});
print('CHANGES IS : $changes ');
}
String? changes Contains Your app State! Be Happy You Can what you want , This can notify Online status like Whatsapp Messenger!
MyCode :
isBg || isScreen == true || isClosed == false
? setState(() {
changes = "User is online"
})
: setState(() {
changes = "User is Offline"
});
print('CHANGES IS : $changes ');
You can catch in onerror.
You have to chenge souce as server
source: Source.server
users.get(GetOptions(source: Source.server))
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final CollectionReference users = _firestore.collection('users');
users.get(GetOptions(source: Source.server))
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
print(doc["first_name"]);
print(doc["last_name"]);
print(doc["gender"]);
print(doc["phone_number"]);
});
}).onError((error, stackTrace) {
print(error.toString());//Offline
Global.showSnackBar(context, error.toString(), false);
});