Show the introduction page only when the user logs in for the first time for a Flutter app - flutter

I have used the below code to show the introduction page only when the user logs in for the first time and second time when the user logs in the user should be taken directly to the homepage.
But this does not work after my first try. Now every time a new user logs in it goes directly to the homepage but not to the introduction page. Please let me know if I am doing anything wrong.
Here is my Code:
class OneTimeScreen extends StatefulWidget {
#override
OneTimeScreenState createState() => new OneTimeScreenState();
}
class OneTimeScreenState extends State<OneTimeScreen>
with AfterLayoutMixin<OneTimeScreen> {
Future checkFirstSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _seen = (prefs.getBool('seen') ?? false);
if (_seen) {
Navigator.of(context)
.pushReplacement(MaterialPageRoute(builder: (context) => HomePage()));
} else {
await prefs.setBool('seen', true);
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => IntroVideoPage()));
}
}
#override
void afterFirstLayout(BuildContext context) => checkFirstSeen();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}

This is because the scope of the boolean which you are storing in SharedPreferences as seen is app-wide. It does not differentiate if userA or userB logs in. It is a single boolean for both.
To make that boolean user specific, we can add a unique userID as a prefix to the key seen like..
bool _seen = (prefs.getBool(userID + 'seen') ?? false);
prefs.setBool(userID + 'seen', true);
This will ensure storing different boolean for each user.

Related

Why Won't This StreamBuilder Display Data without Restarting the App - Revised Code for Review

CURRENT BEHAVIOR: When I log out of my app and then log back in as a different user, I am taken to a dashboard page with no data. I have to restart the app from the IDE in order to load the user's data.
DESIRED BEHAVIOR: When I log in as a given user, the dashboard page should show that user's data without having to reload/restart/refresh anything.
Based on feedback in this thread, I've condensed my code as much as possible while trying not to remove anything that might help identify my issue. Apologies for the ugliness of the code - I removed as much white space and formatting as I could in order to shorten the paste.
I am working on an app that uses the Firebase Realtime Database as a back-end. The app is user-based, so each user will have a directory, with several subdirectories within each user directory. I'm trying to display a simple list of items returned from the database. Currently I have to restart the app each time I log out and log back in as a different user, which isn't what I'm looking for. I need a given user's data to appear upon login. I don't quite understand what all is happening here (I stumbled across a functional solution after several days of trial and error and googling), but I thought a Stream was more or less a 'live' stream of data from a particular source.
The code snippet below is actually taken from three or four different files in my project; I've put everything in one file and stripped out formatting and white space to make it more compact. I don't think I removed anything material to my problem.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const FlipBooks());}
class FlipBooks extends StatelessWidget {const FlipBooks({super.key});
#override
Widget build(BuildContext context) => const MaterialApp(home: AuthService());}
class AuthService extends StatelessWidget {const AuthService({super.key});
static String getUid() => FirebaseAuth.instance.currentUser!.uid;
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {return const DashboardPage();
} else {return const LoginPage();}}))}}
class DashboardPage extends StatelessWidget {const DashboardPage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
child: GestureDetector(onTap: () {FirebaseAuth.instance.signOut();},
child: const Icon(Icons.logout))]),
body: StreamBuilder(
// kPAYEES_NODE is defined in constants.dart as
// kUSER_NODE.child('payees')
// kUSER_NODE is defined as FirebaseDatabase.instance.ref('users/${AuthService.getUid()}')
stream: kPAYEES_NODE.onValue,
builder: (context, snapshot) {
final payees = <Payee>[];
if (!snapshot.hasData) {return Center(child: Column(children: const [Text('No Data')]));
} else {
final payeeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>;
payeeData.forEach((key, value) {
final dataLast = Map<String, dynamic>.from(value);
final payee = Payee(id: dataLast['id'], name: dataLast['name'], note: dataLast['note']);
payees.add(payee);});
return ListView.builder(
shrinkWrap: true,
itemCount: payees.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(payees[index].name), subtitle: Text(payees[index].id));});}}),
floatingActionButton: FloatingActionButton(
onPressed: () {Navigator.push(context, MaterialPageRoute(
builder: (context) => AddThing(), fullscreenDialog: true));},
child: const Icon(Icons.add));}}
class LoginPage extends StatefulWidget {const LoginPage({super.key});
#override
State<LoginPage> createState() => _LoginPageState();}
class _LoginPageState extends State<LoginPage> {
// variables for FocusNodes, TextEditingControllers, FormKey
Future signIn() async {
try {await FirebaseAuth.instance.signInWithEmailAndPassword(email, password);
} on FirebaseAuthException catch (e) {context.showErrorSnackBar(message: e.toString());}}
Future signUp() async {
try {await FirebaseAuth.instance.createUserWithEmailAndPassword(email, password);
} on FirebaseAuthException catch (e) {context.showErrorSnackBar(message: e.toString());}}
Future sendEm() async {
var methods = await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
if (methods.contains('password')) {return signIn();
} else {showDialog(...); // give user option to register or try again
return;}}
Future passwordReset() async {
try {await FirebaseAuth.instance.sendPasswordResetEmail(email);
showDialog(...); // show reset email sent dialog
} on FirebaseAuthException catch (e) {context.showErrorSnackBar(message: e.toString());}}
#override
void dispose() {...}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(...), // logo, welcome text
Form(...), // email+pw fields, forgot pw link => passwordReset(), submit button => sendEm()
]))));}}
As I said in my answer to your previous question, you're defining static String getUid() => FirebaseAuth.instance.currentUser!.uid; which means that is only evaluates the current user once. Since the user can sign in and out, the UID is a stream and that requires that you use authStateChanges to expose it to your DashboardPage, just as you already do in the AuthService itself.
class AuthService extends StatelessWidget {const AuthService({super.key});
static Stream<String?> getUid() => FirebaseAuth.instance. authStateChanges().map<String?>((user) => user?.uid);
...
}
I didn't run the above code, so there might be some typos or minor errors in it.
Now you can use a StreamBuilder when you call getUid() and get a stream of UID values (or null when no one is signed in).

Go back to login when logged out from the drawer, no matter what

I need to redirect user to login page when he clicks on logout button from drawer (wherever he is). The problem is that when I click on the logout button, the screen remains the same.
According to this post: Flutter provider state management, logout concept
I have:
void main() async {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Profile>(
create: (final BuildContext context) {
return Profile();
},
)
],
child: MyApp(),
),
);
}
MyApp:
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
initPlatformState();
}
/// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
if (!mounted) return;
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
initialRoute: '/',
navigatorKey: navigatorKey,
// ...
home: Consumer<Profile>(
builder: (context, profile, child){
return profile.isAuthenticated ? SplashScreen() : AuthScreen();
}
)
);
}
}
The part of the drawer where there is the logout button:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
}),
As I said, when I click on the logout button from the drawer, the user is correctly logged out, but the screen remains the same.
UPDATE
This is the profile class:
class Profile with ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated {
return this._isAuthenticated;
}
set isAuthenticated(bool newVal) {
this._isAuthenticated = newVal;
this.notifyListeners();
}
}
I think you are using provider class incorrectly.
use your profile class like this.
class Profile with ChangeNotifier {
bool _isAuthenticated = true;
bool get getIsAuthenticated => _isAuthenticated;
set setIsAuthenticated(bool isAuthenticated) {
_isAuthenticated = isAuthenticated;
notifyListeners();//you must call this method to inform lisners
}
}
in set method call notifyListners();
in your listTile
replace profile.isAuthenticated = false to profile.isAuthenticated = false;
Always use getters and setters for best practice.
I hope this is what you were looking for.
Add Navigator.of(context).pushReplacementNamed("/routeName") in LogOut onTap() Section.
For more information : https://api.flutter.dev/flutter/widgets/Navigator/pushReplacementNamed.html
Make sure to have logout route set in MyApp file, and i'd edit logout button file as such:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
// add login file route here using Navigator.pushReplacementNamed() ;
}),
Navigator push named -> logout route?

How to use shared preferences to have a one-time login in flutter?

I searched google/stackoverflow alot and tried but nothing worked. This is the flow i want:
User launches the app. Splash screen appears.
Login screen appears after splash screen. User logs in.
User kills(closes) the app.
When user relaunches the app, it should show the splash screen followed by the homepage, as user has already logged in once before.
User will only see login page if he/she logs out.
So far 1 and 2 works. But when user kills/closes the app and relaunches it again, instead of being directed to the home page, they are directed to the login page again.
The code for the splash screen:
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
startTimer();}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 150,
width: 150,
child: new SvgPicture.asset(
'assets/logo.png'
),
),
),
);
}
void startTimer() {
Timer(Duration(seconds: 3), () {
navigateUser(); //It will redirect after 3 seconds
});
}
void navigateUser() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
var status = prefs.getBool('isLoggedIn');
print(status);
if (status == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => HomePage());
} else {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => LoginScreen()));
}
}}
The code for the log out button:
void logoutUser() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs?.clear();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (BuildContext context) => SplashScreen()),
ModalRoute.withName("/login"),
);
}
Sorry for the lengthy post, and really appreciate the help if someone could point out where i've gone wrong. Or if there's any other way to achieve a one-time login in flutter. Thanks!
I know my answer is late now. But, If you are using FirebaseAuth, this will automatically cache your login, logout log. So you will not need to store it to pref. Just nee to make additional step when you lauch screen to check if user's last status was login or log out by the following. And this information can be used to rediret to the desired screen.
Code:
Future<bool> checkIfAlreadySignedIn () async {
late bool _isAlreadySignedIn;
await FirebaseAuth.instance
.authStateChanges()
.listen((event) async {
if (event == null) {
print('Currentyl signed out');
_isAlreadySignedIn = false;
} else {
_isAlreadySignedIn = true;
}
});
return _isAlreadySignedIn;
}
Where do you set 'isLoggedIn' pref to true?

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.

How can i set loading screen when apps check login status?

I use SharedPreferences to keep login status. its works fine.
but after close my app when I open this. it's showing all my screen like flash screen then its stay on its right screen.
but I don't want this and this not good.
I want when apps check login status its shows a loading screen or anything then after completing its show apps right screen.
how can I do this?
Here is my login status check code
checkLoginStatus() async {
sharedPreferences = await SharedPreferences.getInstance();
if (sharedPreferences.getString("empid") == null) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => Home()),
(Route<dynamic> route) => false);
}
}
Snippet
void main async {
bool isUserLogin = await User.isUserLogin();
if (isUserLogin) { // wait untill user details load
await User.currentUser.loadPastUserDetails();
}
runApp(MyApp(isUserLogin));
}
and
class MyApp extends StatelessWidget {
bool isUserLogin;
MyApp(this.isUserLogin);
#override
Widget build(BuildContext context) {
var defaultRoot = isUserLogin ? HomePage() : LoginScreen();
final material = MaterialApp(
debugShowCheckedModeBanner: false,
home: defaultRoot,
);
return material;
}
}
Hope this helps!
Personnally I like to use a variable _isLoading which I set to true at the beginning of my login method using a setState. Then using this bool you just have to change what is displayed in your Scaffold.
bool _isLoading = false;
checkLoginStatus() async {
setState() => _isLoading = true;
sharedPreferences = await SharedPreferences.getInstance();
if (sharedPreferences.getString("empid") == null) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) => Home()),
(Route<dynamic> route) => false);
} else {
setState() => _isLoading = false;
}
}
EDIT: In your case this might be more relevant:
#override
Widget build(BuildContext context) {
switch (currentUser.status) {
case AuthStatus.notSignedIn:
return LoginPage();
case AuthStatus.signedIn:
return HomePage();
default:
return LoadingPage();
}
}
My currentUser is just a class that I've created containing an enum AuthStatus which can have the value notSignedIn or signedIn. If you set the status to null while loading you can display your loading screen.