Page not disposed when signed up: Flutter - flutter

I am trying to implement provider package to signUp/signIn/signOut using Firebase Auth.
My ChangeNotifier class is-
import 'package:e_shoppie/db/authentication.dart';
import 'package:e_shoppie/db/user_services.dart';
import 'package:e_shoppie/structure/constants.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
//User can only be one of these states
/**
* uninitialized: User just opened the app (just opening)
* unauthenticated: Show the login screen to the user
* authenticating: Show a circular indicator the user
* authenticated: User is looged into the app
*/
enum Status { uninitialized, authenticated, authenticating, unauthenticated }
class UserProvider with ChangeNotifier {
FirebaseAuth _auth;
Auth _userAuth = Auth();
UserServices userServices = UserServices();
User? _user;
GoogleSignIn _googleSignIn = GoogleSignIn();
Status _status =
Status.uninitialized; //when the instance of the class is created
UserProvider.initialize() : _auth = FirebaseAuth.instance {
//subscribing to stream to listen to changes in user status
_auth.authStateChanges().listen(
(user) {
_onStatusChanged(user);
},
);
}
Status get status => _status;
User? get user => _user;
Future<bool> signUp(String username, String email, String password) async {
try {
//change the status of the user
_status = Status.authenticating;
//notify the listeners
notifyListeners();
// UserCredential credential =
await _auth.createUserWithEmailAndPassword(
email: email, password: password);
Map<String, dynamic> values = {
'name': username,
'email': email,
'id': user!.uid,
};
userServices.createUser(values);
_status = Status.authenticated;
notifyListeners();
return true;
} catch (e) {
_status = Status.unauthenticated;
notifyListeners();
print(e.toString());
return false;
}
}
Future signOut() async {
print('entered signOut');
await _auth.signOut();
_status = Status.unauthenticated;
notifyListeners();
print('Exiting signOut');
return Future.delayed(Duration
.zero); //duration to return is set to zero (can work without it)
}
Future<void> _onStatusChanged(User? user) async {
if (user == null) {
_status = Status.unauthenticated;
} else {
_user = user;
_status = Status.authenticated;
}
notifyListeners();
}
}
The way I am navigating on state change is-
class ScreenController extends StatelessWidget {
const ScreenController({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context);
switch (user.status) {
case Status.uninitialized:
return SplashScreen();
case Status.unauthenticated:
return LoginScreen();
case Status.authenticating:
return LoadingScreen();
case Status.authenticated:
return HomePage();
default:
return LoginScreen();
}
}
}
Problem: in my SignUp page, I call the signUp method of the UserProvider class to signUp the user.
I expect the signup page gets destroyed and home page appears when user is created and sign up procedure is complete.
What I get: Home Page is built but the sign up page is not destroyed and remains on the screen unless I press the back button.
Sign Up button -
// minWidth: MediaQuery.of(context).size.width.,
child: Text(
'Sign Up and Register',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
onPressed: () async {
if (!await provider.signUp(
_nameTextController.text,
_emailTextController.text,
_passwordTextController.text)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Sign In Failed')));
}
// Navigator.pop(context);
// Navigator.pushReplacement(
// context,
// MaterialPageRoute(
// builder: (context) => HomePage()));
},
),
Also- my Sign Up class is wrapped with Consumer.
I am using the provider package for navigation. The problem I am facing is: that Debug mode shows that the login page is disposed of when the user logs in and Home Page appears. But when I sign in from the Sign Up page, the page is not disposed and Home Page is constructed below it.
Please help!!

You have to use Navigator.popAndPushNamed(context) or Navigator.pushReplacementNamed(context). This is work perfect in your scenario both have the same output difference is only animations. For signUp/signIn/signOut this is ideal way.

Related

How to use sharedpreferences to save users roles and navigate to a specific page depending on role in Flutter

I'm working on app that have user logins (Admin login and user login). First i make a user part and it works, the account keep logged even when the app restart. and then when i have to separate the users (admin and user) i got some problem. I don't know how to code the shared preferences, this is the code when i make a user part
preference_helper.dart
import 'package:shared_preferences/shared_preferences.dart';
class PreferencesHelper {
final Future<SharedPreferences> sharedPreferences;
const PreferencesHelper({required this.sharedPreferences});
static const String login = 'LOGIN';
void setIsLogin(bool value) async {
final prefs = await sharedPreferences;
prefs.setBool(login, value);
}
Future<bool> get isLogin async {
final prefs = await sharedPreferences;
return prefs.getBool(login) ?? false;
}
}
i use the provider like this
preference_notifier.dart
class PreferencesNotifier extends ChangeNotifier {
PreferencesHelper preferencesHelper;
PreferencesNotifier({required this.preferencesHelper}) {
_getIsLogin();
}
bool _isLogin = false;
bool get isLogin => _isLogin;
void _getIsLogin() async {
_isLogin = await preferencesHelper.isLogin;
notifyListeners();
debugPrint(_isLogin ? 'isLogin true' : 'isLogin false');
}
void setIsLogin(bool value) async {
preferencesHelper.setIsLogin(value);
_getIsLogin();
}
}
i want to use shared preferences to save the user roles and navigate to specific page. So if the user's log in it will go to the UserHomePage and if the admin log in it will go to the AdminHomePage. My backend is firebase firestore.
this is part of sign page (when click register button)
MaterialButton(
color: primaryColor,
textTheme: ButtonTextTheme.primary,
height: 40,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
onPressed: () async {
setState(() {
_isLoading = true;
});
try {
final navigator = Navigator.of(context);
final email = _emailController.text;
final password = _passwordController.text;
const role = "user";
await _auth
.createUserWithEmailAndPassword(
email: email,
password: password,
)
.then((value) => {postDetailsToFirestore(email, role)});
navigator.pop();
} catch (err) {
final snackBar = SnackBar(content: Text(err.toString()));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} finally {
setState(() {
_isLoading = false;
});
}
},
child: const Text('Signup'),
),
postDetailsToFirestore(String email, String role) async {
FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
var user = _auth.currentUser;
CollectionReference ref = firebaseFirestore.collection('users');
ref.doc(user!.uid).set({'email': _emailController.text, 'role': role});
}
this is the login page (when click the login button)
MaterialButton(
color: primaryColor,
textTheme: ButtonTextTheme.primary,
height: 40,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
onPressed: () async {
setState(() {
_isLoading = true;
});
try {
final navigator = Navigator.of(context);
final email = _emailController.text;
final password = _passwordController.text;
await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
route();
value.setIsLogin(true);
navigator.pushReplacementNamed(HomePage.routeName);
} catch (err) {
final snackBar = SnackBar(content: Text(err.toString()));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} finally {
setState(() {
_isLoading = false;
});
}
},
child: const Text('Login'),
),
i want to navigate the navigator to specific user role
this is the route() function
void route() {
User? user = FirebaseAuth.instance.currentUser;
FirebaseFirestore.instance.collection('users').doc(user!.uid).get().then(
(DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists) {
if (documentSnapshot.get('role') == "user") {
Navigator.pushNamed(context, UserHomePage.routeName);
} else {
Navigator.pushNamed(context, AdminHomePage.routeName);
}
} else {
debugPrint('Document does not exist on the database');
}
},
);
}
and this is the main.dart at runApp()
runApp(
await preferencesHelper.isLogin
? const MyApp(
pageRouteName: HomePage.routeName,
)
: const MyApp(
pageRouteName: LoginPage.routeName,
),
);
I really need to know how am i supposed to do because this is for my exam. I'm sorry if my english is bad, i'm barely use English to talk. Thank you
that code that i share is what i tried to make sharedpreferences but it just for 1 user, i dont know how to separate user (admin and user)
First of all, you need to use architecture to separate the UI from logic and in your architect, you have to create a layer to handle basic requests of the local database and then create a class for implementing basic commands of the database, then you can create a separated storage layer for each of entities that you have.
the abstract basic commands class is like this :
abstract class LocalStorage {
Future<void> write(final String key, final dynamic json);
dynamic read<S>(final String key);
void remove(final String key);
void removeAll();
}
and for implementation :
class StorageService implements LocalStorage {
StorageService() {
_init();
}
late GetStorage storage;
void _init() {
storage = GetStorage();
}
#override
Future<void> write(final String key, final dynamic value) async {
await storage.write(key, convert.jsonEncode(value));
}
#override
dynamic read<S>(final String key) {
final value = storage.read(key);
if (value == null) return;
return convert.jsonDecode(value.toString());
}
#override
void remove(final String key) {
GetStorage().remove(key);
}
#override
void removeAll() {
GetStorage.Remove(key1);
GetStorage.Remove(key2);
...
}
}
and for Usage for each entity:
class UserStorage {
final LocalStorage _storage;
Future<void> SaveUser(User usr) async {
await _storage.write(userKey, usr);
}
}
I have used GetX to handle local storage for read and write but you can replace your preferred shared preference library.

Flutter unable to login after create new account

After building the app, if I try to login it's working fine, but if I create a new account then try to login again, login is not working. I have seen after debugging that I'm getting token in _authenticate method. I think notifyListeners not working here but I don't know why?
I'm using the provider package in my flutter app. I have an Auth provider class where I'm saving data in firebase and also login by firebase. Below it's my provider class.
class Auth with ChangeNotifier {
String? _token;
late DateTime _expiryDate = DateTime.now();
late String _userId;
bool get isAuth {
return token != null;
}
String? get token {
if ((_expiryDate).isAfter(DateTime.now())) {
return _token;
}
return null;
}
Future _authenticate(String email, String password, String urlSegment) async {
var urlParse = Uri.parse(urlSegment);
try {
final response = await http.post(urlParse,
body: jsonEncode({
'email': email,
'password': password,
'returnSecureToken': true
})
);
final responseData = jsonDecode(response.body);
if (responseData['error'] != null) {
throw HttpException(responseData['error']['message']);
}
// set token and user id from firebase response
_token = responseData['idToken'];
_userId = responseData['localId'];
_expiryDate = DateTime.now()
.add(Duration(seconds: int.parse(responseData['expiresIn'])));
notifyListeners();
return responseData['idToken'];
} catch (error) {
rethrow;
}
}
Future login(String email, String password) async {
print(email);
String url = Constants.firebaseLoginUrl;
return _authenticate(email, password, url);
}
Future signup(String email, String password) async {
String url = Constants.firebaseSignupUrl;
return _authenticate(email, password, url);
}
}
In signUp page I have tried below code to create a new user
Future<void> signUpSubmit() async {
if (_formKey.currentState!.validate()) {
await Provider.of<Auth>(context, listen: false).signup(_email.text, _pass.text);
}
I have checked new data is saving perfectly.
In signUp page there has a login button, after click on login button I have redirect in login page,
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LoginPage()
),
);
},
child: const Text('Log in to access'),
)
After click on login I have redirect in login page, then tried login again but it's not working. In debug I'm getting token if I print(token) in _authenticate method.
In main.dart my consumer is looks like
child: Consumer<Auth>(
builder: (ctx,auth, _) => MaterialApp(
home: auth.isAuth ? const HomePage():const LoginPage(),
)
After create account if I rebuild app again then login is working? How I will solve this problem?
See you are redirecting directly in loginPage rather than via the main page ! Just redirect the login button to main page, in your main page there has condition
home: auth.isAuth ? const HomePage(): const LoginPage()
So, if it is auth false it will always redirect to the login page.
Change
MaterialPageRoute( builder: (context) => const LoginPage() ),
To
MaterialPageRoute( builder: (context) => const MainPage() ),

State managment in flutter with consumer and scaffoldState

I'm using the Provider dependencie to manage states on my screen. Currently I have created a Loading Screen that works with Lottie animation. In my Sign In page, whenever there is an error with the log in, a Snackbar is shown to the user. Althought now, when I use the splash screen, the screen doesn't return and the snackBar isn't shown.
This is a piece of the login screen:
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: RaisedButton(
onPressed: userManager.loading
? null
: () {
if (formKey.currentState!
.validate()) {
userManager.signIn(
user: User(
email:
emailController.text,
password:
passController.text),
onFail: (e) {
scaffoldKey.currentState!
.showSnackBar(SnackBar(
content: Text(
'Falha ao entrar: $e'),
backgroundColor:
Colors.red,
));
},
onSucess: () {
debugPrint(
'Sucesso ao Logar');
Navigator.of(context).pop();
});
}
},
On the onFail I get this error, whenever I have a wrong password or other datas wrong:
Ocorreu uma exceção.
_CastError (Null check operator used on a null value)
This is how I'm changing between pages:
class LoginScreen extends StatelessWidget {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController emailController = TextEditingController();
final TextEditingController passController = TextEditingController();
#override
Widget build(BuildContext context) {
return Consumer<UserManager>(builder: (_, userManager, child) {
if (userManager.loading) {
return SplashScreen();
} else {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
UserManager:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:loja_virtual_nnananene/helpers/firebase_errors.dart';
import 'package:loja_virtual_nnananene/models/user.dart';
class UserManager extends ChangeNotifier {
UserManager() {
_loadCurrentUser();
}
final FirebaseAuth auth = FirebaseAuth.instance;
User? user;
bool _loading = false;
bool get loading => _loading;
bool get isLoggedIn => user != null;
Future<void> signIn(
{required User user,
required Function onFail,
required Function onSucess}) async {
loading = true;
try {
final AuthResult result = await auth.signInWithEmailAndPassword(
email: user.email!, password: user.password!);
await _loadCurrentUser(firebaseUser: result.user);
onSucess();
} on PlatformException catch (e) {
onFail(getErrorString(e.code));
}
loading = false;
notifyListeners();
}
Future<void> signUp(
{required User user,
required Function onFail,
required Function onSucess}) async {
loading = true;
try {
final AuthResult result = await auth.createUserWithEmailAndPassword(
email: user.email!, password: user.password!);
user.id = result.user.uid;
this.user = user;
await user.saveData();
onSucess();
} on PlatformException catch (e) {
onFail(getErrorString(e.code));
}
loading = false;
notifyListeners();
}
void signOut() {
auth.signOut();
user = null;
notifyListeners();
}
set loading(bool value) {
_loading = value;
notifyListeners();
}
Future<void> _loadCurrentUser({FirebaseUser? firebaseUser}) async {
final FirebaseUser currentUser = firebaseUser ?? await auth.currentUser();
if (currentUser != null) {
final DocumentSnapshot docUser = await Firestore.instance
.collection('users')
.document(currentUser.uid)
.get();
user = User.fromDocument(docUser);
final docAdmin = await Firestore.instance
.collection('admins')
.document(user!.id!)
.get();
if (docAdmin.exists) {
user!.admin = true;
}
notifyListeners();
}
}
bool get adminEnabled => user != null && user!.admin;
}
Is there another way to set the splash screen thats easier?
While I wait for you to add the UserManager class implementation, I think there's a missing notifyListeners() in the signIn method.

Fetch data from firestore after Sign In before showing HomeScreen

When a User sign up for the first time, i want that he gets a own firestore document with some data. This data I want to show on the homescreen but I get an error that the data is not there yet. After hot reload the data is there so the problem is that the homescreen is shown before the data is fetched from firestore although I use a FutureBuilder. I only got this problem when a user signs in for the first time.
class GoogleSignInProvider extends ChangeNotifier {
final googleSignIn = GoogleSignIn();
GoogleSignInAccount _user;
GoogleSignInAccount get user => _user;
Future googleLogin() async {
try {
final googleUser = await googleSignIn.signIn();
if (googleUser == null) return;
_user = googleUser;
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
await FirebaseAuth.instance.signInWithCredential(credential);
await getUserData();
} catch (e) {
print(e.toString());
}
notifyListeners();
}
Future getUserData() async {
final myUser = FirebaseAuth.instance.currentUser;
bool userExist = await checkIfUserExists(myUser.uid);
if (userExist == false) {
print('User dont exist');
await FirebaseFirestore.instance.collection('users').doc(myUser.uid).set({
"email": myUser.email,
"plans": [],
"userScore": "100",
});
} else {
print('User exist');
}
/// Save userData from firestore in a Helper class which is shown on the homescreen
var userData = FirebaseFirestore.instance.collection('users').doc(myUser.uid);
return FutureBuilder(
future: userData.get(),
builder: (context, userDataSnapshot) {
if (userDataSnapshot.data == ConnectionState.done) {
var value = userDataSnapshot.data;
UserManager.userdata = value.data(); //static class where userData is stored
return null;
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
});
}
Future<bool> checkIfUserExists(String uid) async {
try {
var collectionRef = FirebaseFirestore.instance.collection('users');
var doc = await collectionRef.doc(uid).get();
return doc.exists;
} catch (e) {
throw e;
}
}
EDIT
This is the Button where the user can sign In
ElevatedButton.icon(
onPressed: () {
final provider = Provider.of<GoogleSignInProvider>(context, listen: false);
provider.googleLogin();
},
icon: Icon(MdiIcons.google),
label: Text(
'Sign In with Google',
style: TextStyle(fontSize: 16),
),
),
The problem here is that FutureBuilder is a widget, it should not be used in a function to wait for a future to complete, but in another widget to have callbacks on the completion and change display based on that.
If not rendered, FutureBuilder will do nothing but be instantiated and occupy memory.
You should probably modify your code as such:
...
/// Save userData from firestore in a Helper class which is shown on the homescreen
var userData = await FirebaseFirestore.instance.collection('users').doc(myUser.uid).get();
UserManager.userdata = userData.data();
...
Should you want to add a CircularProgress on your main screen, this would be done by lisntening to your Provider in some way.

Flutter onAuthStateChanged stream not updating child when registering or signing in

I wrapped my app inside a StreamProvider.value, where the value is an onAuthStateChanged stream, and the child is the main MaterialApp. My main wrapper listens to this value using Provider.of. This works fine for signing out; once I sign out, the StreamProvider notifies my main wrapper, and the app is redirected to the welcome screen.
But when I try to create an account or sign in, it seems the StreamProvider is not being notified of the onAuthStateChanged stream. This is why it's so weird, if the stream wasn't working, then signing out should also not work. Also, when I hot restart, I do get redirected to the home screen. I've scattered the internet: no luck. Here's the appropriate code:
In main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<FirebaseUser>.value(
value: AuthService().userStream,
child: MaterialApp(
home: Wrapper(),
theme: ThemeData.light().copyWith(
appBarTheme: AppBarTheme(color: Colors.lightBlue),
scaffoldBackgroundColor: Colors.white,
),
),
);
}
}
In wrapper.dart
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Rebuilding');
final user = Provider.of<FirebaseUser>(context);
if (user == null) {
return Welcome();
} else {
return Home();
}
}
}
In auth_service.dart
class AuthService {
FirebaseAuth _auth = FirebaseAuth.instance;
Stream<FirebaseUser> get userStream {
return _auth.onAuthStateChanged;
}
Future registerWithEmailAndPassword(String email, String password) async {
try {
AuthResult result = await _auth.createUserWithEmailAndPassword(
email: email, password: password);
FirebaseUser user = result.user;
return user;
} catch (e) {
print(e.toString());
return null;
}
}
Future signInWithEmailAndPassword(String email, String password) async {
try {
AuthResult result = await _auth.signInWithEmailAndPassword(
email: email, password: password);
FirebaseUser user = result.user;
return user;
} catch (e) {
print(e.toString());
return null;
}
}
void signOut() {
try {
_auth.signOut();
} catch (e) {
print(e);
}
}
}
Also, inside my welcome.dart, I have this set up as a test:
onPressed: () {
FirebaseAuth.instance.onAuthStateChanged
.listen((event) {
print('$event from .listen');
});
}
And this does notify my everytime I sign in, out or register, this tells me that there is nothing wrong with onAuthStateChanged, or the way I set it up.
If you need any more information, let me know. I don't want to make the question any longer. Thank you for your help!