how to disable user to go back after successful login in flutter - flutter

I have this login function and it's working like charm , but the problem that the user can press the back button and return to the login screen and I want to disable that.
void _login() async {
setState(() {
_isLoading = true;
});
var data = {'email': email, 'password': password};
print("Data =" + data.toString());
var res = await Network().authData(data, '/login');
var body = json.decode(res.body);
print(body);
if (body['success']) {
SharedPreferences localStorage = await SharedPreferences.getInstance();
localStorage.setString('access_token', json.encode(body['access_token']));
localStorage.setString('user', json.encode(body['user']));
if (body['user']['verified'] == 1) {
// OPEN THE HOME PAGE AND BLOCK THE BACK ACTION
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
);
} else {
showAnimatedDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
content: Container(
width: 100,
height: 50,
child:
Center(child: Text("Your account isn't verified yet!")),
));
},
animationType: DialogTransitionType.fadeScale,
curve: Curves.fastOutSlowIn,
duration: Duration(seconds: 1),
);
}
} else {
_showMsg(body['message']);
}
setState(() {
_isLoading = false;
});
}
Plus , is there a way also to keep him logged even after closing the app once the login is done ? (Until the user press a logout button).

Firstly: Let us understand how Screens are organized in Flutter Framework:
The screens in Flutter are organized in a Stack fashion, so if you are on Screen A and you pushed the Screen B, then from Screen B you pushed Screen C, your stack would look like the following:
After Pushing Screen B:
Screen A
Screen B (Current Screen)
After Pushing Screen C:
Screen A
Screen B
Screen C (Current Screen)
So let's say Screen B is your Login Screen, and you want to block the user from going back, Then you don't have to just Push the screen into the stack, instead, you should Replace the screen in the stack with your Home Screen.
Secondly: Now after understanding the flow, let us talk about the code:
In your scenario, instead of using Navigator.push() you have to use Navigator.pushReplacement() to replace the screen in the stack as mentioned above.
Bounce:
If you are in a scenario that needs all the screens to be removed from the stack and keep only the screen that would be pushed, as an example, if you have a multi-step registration flow, and after successful registration, you want to pop all the screens of the process from the stack and pushing the home screen, then you may use the following trick:
Navigator.popUntil((route) => route.isFirst); // Execute it before pushing the screen in order to keep only one route (screen) in the stack.
Navigator.pushReplacement(); // Now you are replacing the only screen in the stack with the new one so the stack will only contain your wanted screen.

I kind of find a way to do it , I don't know if it's the good way to do it but this did the job .
Wrapping the scaffold with WillPopScope like this :
WillPopScope(
onWillPop: () async => false,
)
I got it from here

Here first you need to check whether the user is authenticated....
You have to check this condition's into material app's home : parameter.
if (snapShot.connectionState == ConnectionState.waiting) {
return const SplashScreen(); // If it's in waiting state it will show splash screen
}
if (snapShot.hasData) {
return const homeScreen(); // If user is authenticated it will show home screen
}
return const LoginScreen(); // If user is not authenticated then it will show login screen
here, snapShot is all about user data.
Thanks in advance.

To block the user from moving back to the previous screen, try Navigator.pushReplacement instead of Navigator.push.

Related

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?

How to close a screen from PageView class flutter

Greeting,
I have a really specific question to ask. I have to explain it with steps and pictures so there they are.
I have an app with three screens:
Main Feed Screen,
Main Chat and Requests Screen,
Main Profile Screen,
And they are all a part of a PageView. This PageView class is controlled inside of a class called main_tab_controller.dart. In that class, in initState(), I have a Firebase Messaging method that is called every time I get a notification (onMessage). So, every time I get this notification, I show an overlay that looks like this.
And it works perfectly on these three main screen. If it's a chat notification, I will direct the PageView to the second screen i.e MainChatAndRequest Screen, and open the chat screen. If it's a request notification, I will direct the PageView to the second screen i.e MainChatAndRequest Screen, and open the requests screen.
But the issue that I am having is the following. In my MainFeedScreen and MainProfileScreen, I have some other screens that I open. For example in MainFeedScreen, I open UserDetailsScreen or FilterScreen. Or in the MainProfileScreen, I open SettingsScreen or EditUserProfileScreen.
So my question is: For example, if I navigate to MainProfileScreen and in that screen open SettingsScreen, and I get the overlay top message, how do I close the SettingsScreen that is currently open and navigate back to the second screen i.e MainChatsAndRequestsScreen from the Firebase Messaging Function that is in initState() of main_tab_controller.dart that is the parent to all of the other screens.
You have the Image Below:
I have tried everything, Navigator.popUntil(context), Navigator.pushReplacement(context), used Navigator.pushNamed(context) but nothing worked. If someone can help me, it would be much appreciated.
Just to give you the better undertanding of the screens:
The Parent Screen is the PageView with three screens:
Main Feed Screen
Main Chat and Requests Screen
Main Profile Screen
and then in Main Feed Screen you have:
Filters Screen
Profile Details Screen
in Main Chat and Requests Screen you have two TabBar Screens:
Chats Screen
Requests Screen
and in Main Profile Screen you have:
Settings Screen
Edit Profiles Screen
PageView Code Snippet:
#override
void initState() {
pageController = PageController(initialPage: _currentIndex);
chatAndRequestController = TabController(length: 2, vsync: this);
var chatAndRequestProvider =
Provider.of<ChatAndRequestProvider>(context, listen: false);
super.initState();
fbm.requestNotificationPermissions();
fbm.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
bool isRequest;
var mode = (Platform.isIOS) ? message['mode'] : message['data']['mode'];
var imageUrl = '';
switch (mode) {
case 'chat':
isRequest = false;
imageUrl =
chatAndRequestProvider.chatsList.first[kProfilePictureUrl];
break;
case 'sentRequest':
isRequest = true;
imageUrl = (Platform.isIOS)
? message['profilePictureUrl']
: message['data']['profilePictureUrl'];
break;
case 'acceptRequest':
isRequest = false;
imageUrl = (Platform.isIOS)
? message['profilePictureUrl']
: message['data']['profilePictureUrl'];
break;
default:
isRequest = false;
break;
}
AudioCache player = new AudioCache();
const alarmAudioPath = "sounds/notification_sound.mp3";
player.play(alarmAudioPath);
print('Show this ting');
if (_currentIndex != 1) {
if (!isDialogOpen) {
isDialogOpen = true;
_showAnimatedBox(
context,
(Platform.isIOS)
? message['aps']['alert']['title']
: message['notification']['title'],
(Platform.isIOS)
? message['aps']['alert']['body']
: message['notification']['body'],
imageUrl,
isRequest,
);
}
}
},
onLaunch: (Map<String, dynamic> message) async {
print("onLaunch: $message");
},
onResume: (Map<String, dynamic> message) async {
print("onResume: $message");
},
);
notificationPlugin
.setListenerForLowerVersions(onNotificationInLowerVersions);
notificationPlugin.setOnNotificationClick(onNotificationClick);
_children.addAll([
MainFeedScreen(
analytics: widget.analytics,
observer: widget.observer,
latitude: widget.latitude,
longitude: widget.longitude,
),
MainChatAndRequestScreen(
analytics: widget.analytics,
observer: widget.observer,
pageContoller: chatAndRequestController,
),
MainProfileScreen(analytics: widget.analytics, observer: widget.observer),
]);
}
Future _showAnimatedBox(context, topText, bottomText, imageUrl, isRequest) {
showDialog(
context: context,
builder: (BuildContext builderContext) {
_timer = Timer(Duration(seconds: 4), () {
Navigator.of(context).pop();
isDialogOpen = false;
});
return Dismissible(
key: Key('dismissible'),
direction: DismissDirection.up,
onDismissed: (_) {
Navigator.of(context).pop();
isDialogOpen = false;
},
child: FunkyNotification(
() {
var chatAndRequestProvider =
Provider.of<ChatAndRequestProvider>(context, listen: false);
// var contextProv =
// Provider.of<ContextProvider>(context, listen: false);
chatAndRequestProvider.setAreThereNewChatsAndRequestFalse();
if (isRequest) {
pageController.jumpToPage(1);
chatAndRequestController.animateTo(1);
Navigator.of(context).pop();
// Navigator.of(contextProv.context).pop();
// SystemChannels.platform.invokeMethod('SystemNavigator.pop');
// Navigator.popUntil(
// context,
// ModalRoute.withName('/mainProfileScreen'),
// );
// Navigator.of(context)
// .popUntil(ModalRoute.withName('/mainProfileScreen'));
// Navigator.pushAndRemoveUntil(
// context,
// MaterialPageRoute(
// builder: (BuildContext context) => MainTabBarController(
// analytics: null,
// observer: null,
// latitude: 100.23423234,
// longitude: 12.324234234,
// isProfileBlocked: false,
// isVersionGood: true,
// ),
// ),
// (route) => true,
// );
} else {
var chatAndRequestProvider =
Provider.of<ChatAndRequestProvider>(context,
listen: false);
pageController.jumpToPage(1);
chatAndRequestController.animateTo(0);
Navigator.of(context).pop();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
appMode:
chatAndRequestProvider.chatsList.first[kAppMode],
peerId: chatAndRequestProvider.chatsList.first[kUserId],
peerAvatar: chatAndRequestProvider
.chatsList.first[kProfilePictureUrl],
peerName: chatAndRequestProvider
.chatsList.first[kNameAndSurname],
friendshipStatus: chatAndRequestProvider
.chatsList.first['friendsStatus'],
analytics: widget.analytics,
observer: widget.observer,
),
),
);
}
},
topText,
bottomText,
imageUrl,
),
);
}).then((val) {
if (_timer.isActive) {
_timer.cancel();
}
isDialogOpen = false;
});
}
I will try make my answer as general as possible in order to make it easier for others to follow along.
The problem in a nutshell is that you have a nested set of screens distributed between a set of pageviews, and you want to switch between the pageviews from an external event (The overlay in this case).
Below is an example:
TL;DR
I couldn't provide the full code since I don't have your full source code. But here is an example 😉
Note: This example uses Provider.
Sample Event Code
// Remove all the screens in the route
Navigator.of(context).popUntil((route) => route.isFirst); // If the screen is not the first replace the check
// Change the second pageview page
Provider.of<ChatSelectPageView>(context, listen: false).setPage(selectedSecondPageViewPage);
// In case it is required to add intermediate screens between the first and the second pageview it must be added here
// Change the main pageview page
_mainPageViewcontroller.animateToPage(1);
Second PageView
// Reads the page index present in the provider
int selectedPage = Provider.of<ChatSelectPageView>(context, listen: false).page;
// Changes to the cotroller page to the selected page
_pageController.jumpToPage(selectedPage);
ChatSelectPageView
class ChatSelectPageView extends ChangeNotifier {
int page = 0;
void setPage(int _page) {
page = _page;
// Depending on your implementation it might be better to remove this line to reduce the number of builds
notifyListeners();
}
}
TS;WM
In order to achieve the desired behavior, there is multiple ways to achieve it. If we want to stick to your implementation we will be a bit constrained. But in this case what I would suggest you do is to use some kind of global state management library such as provider, it can be done without any library but the state will get very messy very quickly.
As you mentioned above you tried Navigator.popUntil but it didn't work, I suspect the reason for this is that you are providing the wrong context. Since Navigator.**** relies on the context to work, i.e. to pop a screen you must provide its context. Or the route check is wrong.
This code is to be written in the external event in your case it will be written in the click listener of the overlay.
Use a state management solution such as Provider to pass the state to the descendants of the main Pageview down to the screens. This provider will be of type ChangeNotifierProvider. When the overlay is clicked, a flag will be set to be the desired pageview page index (I am speaking about the 2nd pageview). In your case this flag is used to select chats or requests.
After that is done you call Navigator.of(context).popUntil((route) => route.isFirst); assuming that the pageview is present on the first page of your app. In the case where it is not on that page, you will have to use Navigator.of(context).popUntil() with a custom logic.
After that we will have to navigate back to the 2nd pageview, or change the first pageview to be the 2nd page in your case. The second pageview will be already switched since we changed the flag in provider before.

Closing ModalBottomSeet before time on Flutter causes black screen

I'm trying to create a modal bottom sheet using showModalBottomSheet, which wil display a form to register a todo item. The idea is that once the todo item is registered, I want to display a check icon from some seconds and then automatically close the sheet.
here is the snippet:
FloatingActionButton _floatingActionButton(BuildContext context) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
await _showBottomSheet(
context: context,
content: CreateTodoForm(
onClose: () {
...
Navigator.pop(context);
},
),
);
},
);
}
and inside the CreateTodoForm widget:
class _CreateTodoFormState extends State<CreateTodoForm> {
TextEditingController titleController = TextEditingController();
bool completed = false;
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<TodoFormBloc>(),
child: BlocBuilder<TodoFormBloc, ITodoFormState>(
builder: (context, state) {
...
if (state is SubmittedTodo) {
Future.delayed(Duration(seconds: 2), widget.onClose);
return Container(
height: 127,
child: Icon(Icons.check, size: 50, color: Colors.white),
);
}
...
},
),
);
}
Has you can see, when the state is SubmittedTodo (todo was submitted successfully) I return a container with the check icon, and after 2 seconds I call the onClose Function which is a call to Navigator.pop(context) to close the sheet.
This works great but it has a problem... if the user taps the < button on the device, or swipe the sheet down to dismiss it, before the 2 seconds are completed, then the sheet closes due to the user action, and then the future completes and it basically closes the app (the app get full black screen).
So my quiestion is how can I close the sheet automatically after some time safely without worring about what the user does.
Probably this is happening because of Navigator.pop(context); getting called after you click the back button which cause two pop. and the black screen is shown because there is no other screen to navigate back to.
As a solution i propose wrapping your form widget by WillPopScope and then you will get notified that the user clicked on the back button. here you can close your form by calling onClose

How does listeners in Provider work in Flutter?

I am using Provider package to work with data from different locations. For this question, I have created a sample project which consists of a Welcome Screen which shows a Login button, upon click, it redirects to Login Screen where one would have login text field but for now, I have placed a button which on click calls the login function and that login function notifies the listeners. I also have a logout function and boolean value which keeps track of the logged in status.
These functions and values reside in a file called auth.provider.dart in providers directory in lib folder.
import 'package:flutter/foundation.dart';
class Auth with ChangeNotifier {
var _loggedIn = false;
bool get loggedIn => _loggedIn;
void loginUser() {
_loggedIn = true;
print('Logged in');
notifyListeners();
}
void logout() {
_loggedIn = false;
print('Logged out');
notifyListeners();
}
}
I am providing the provider in main.dart and also using Consumer:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Auth>(
create: (_) => Auth(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Consumer<Auth>(
builder: (_, auth, __) {
print('main.dart logged in: ${auth.loggedIn}');
if(auth.loggedIn) return HomeScreen();
return WelcomeScreen();
},
),
routes: {
LoginScreen.routeName: (ctx) => LoginScreen(),
HomeScreen.routeName: (ctx) => HomeScreen(),
},
),
);
}
}
In the above file, I simply check whether loggedIn is true or not. If it is not then show the welcome screen so the user can login but if the user is logged in then directly show the home screen. Have also put a print statement in Consumer's builder so it prints the status whenever it gets notified of any changes in auth data. This runs once when the app starts and never again.
There is not much in the login screen, just a scaffold and a login button which calls the login function.
RaisedButton(
child: Text('Click to Login'),
onPressed: () {
Provider.of<Auth>(context, listen: false).loginUser();
},
),
Now on login button click, it sets the loggedIn boolean to true and notifies the listener which should be Consumer but Consumer in main.dart is never notified of it. It does not trigger the change, there is a condition which checks if logged in is true then show home screen but screen does not change at all. One has to manually use Navigator to move to home screen and on home screen there is a logout button which simply sets loggedIn to false, once that happens it should notify the Consumer but it once again doesn't.
Which now brings me to the question: How does listeners in Provider work? When does it notify the listeners for changes? Will it not notify if the changes occur more than a level down like in the above example, Consumer is in main.dart and the changes occur 2 levels down main.dart > WelcomeScreen > LoginScreen. How to let the Consumer in main.dart know that the underlying data it is using has changed?
If you want to quickly get started with the above example, here is the repository link.
I went through your github code.
With pushReplacementNamed to go to login screen
Initially->main.dart->welcomeScreen->(on-clicking-loginbtn)->LoginScreen
Here,
1)Consumer is not listening(because your initial main.dart screen is replaced by login page and doesnt exist on a page currently to listen)
2)There is no navigator to go back(because page is replaced not in a stack)
With pushNamed to go to login screen
Initially->main.dart->welcomeScreen->(on-clicking-loginbtn)->LoginScreen
Here,
1)Consumer is listening(because your initial main.dart screen is not replaced and exist on a page currently.But you are not seeing the changes because you have covered your main intial page with login screen above in the stack )
2)There is navigator to go back to HomeScreen(Now you can go back to see changes using back arrow)
Conclusion Page will not able to listen to changed Provider Value if the page is replaced by other page. It should be there in a screen listening.
Pattern that can be used
main.dart(home argument)
home: Consumer<Auth>(
builder: (_, auth, __) {
print('main.dart logged in: ${auth.loggedIn}');
if(auth.loggedIn) return HomeScreen();
return WelcomeScreen();
},
)
WelcomeScreen(login code)
RaisedButton(
child: Text('Login'),
onPressed: () => Navigator.of(context).pushReplacementNamed(LoginScreen.routeName),
)
LoginScreen(login button)
RaisedButton(
child: Text('Click to Login'),
onPressed: () {
Provider.of<Auth>(context, listen: false).loginUser();
Navigator.of(context).pushReplacementNamed('/');
},
)
HomeScreen(signout btn)
RaisedButton(
child: Text('Logout'),
onPressed: () {
Provider.of<Auth>(context, listen: false).logout();
Navigator.of(context).pushReplacementNamed('/');
}

How to handle progress indicator upon canceling google sign-in dialog?

When user clicks on Google Sign in button, the progress indicator is set to true and the app shows dialog to select user accounts from. If user clicks outside of the dialog or system back button, the progress indicator keeps showing.
Where and how do I set the _isLoading = false so that the progress indicator goes away ?
Container(
padding: EdgeInsets.all(_isLoading ? 20.0 : 0.0),
width: 75,
height: 75,
child: _isLoading
? CircularProgressIndicator()
: IconButton(
icon: Image.asset('assets/google.png'),
onPressed: () => _handleGoogleSignIn()),
)
...
Future<Null> _handleGoogleSignIn() async {
setState(() {
_isLoading = true;
});
LoginUtils().handleGoogleSignIn().then((firebaseUser) {
_postSignInAction(firebaseUser);
}).catchError((exception) {
_handleLoginException(exception);
});
}
This depends on what exactly LoginUtils().handleGoogleSignIn() does. Given that it is a Future, if what it does is display the dialog to select the Google account, it should complete as soon as the dialog is dismissed. I would expect it to complete successfully, with firebaseUser being null - this is the default behavior when popping routes by pressing back as well.
You should therefore simply have to add the following:
if (firebaseUser == null) {
setState(() {
_isLoading = false;
});
}
If however that Future does not complete at all when the dialog is dismissed, then that's the problem you need to solve.