I'm using local_auth for user verification.
The root widget of my app is a stateless widget.
The bug:
The authentication screen pops up as usual. If the fingerprint (or pin) matches, the user can then access the app. However, if the back button is pressed (while the authentication screen is still up), the authentication window vanishes and the user can access the app without authenticating.
I'm using Flutter-2.5.3 and local_auth-1.1.8.
This is the main.dart:
//other imports here
import 'package:local_auth/local_auth.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
var localAuth = LocalAuthentication();
SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.getBool('auth') == true) {
await localAuth.authenticate(
localizedReason: 'Authenticate to access Notes',
useErrorDialogs: true,
stickyAuth: true,);
}
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]).then((_) {
runApp(ProviderScope(child: MyApp()));
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
//returns my widget
}
}
I tried moving the runApp block under a conditional, such that the main root window gets called only when the authentication was successful. However the result remained the same.
This was what I did:
if (prefs.getBool('auth') == true) {
var authenticate = await localAuth.authenticate(
localizedReason: 'Authenticate to access Notes',
useErrorDialogs: true,
stickyAuth: true,);
if (authenticate == true) {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]).then((_) {
runApp(ProviderScope(child: MyApp()));
});
}
}
This is what worked for me:
Changing MyApp to a StatefulWidget.
Adding an awaited function that attempts to authenticate the user before the user can access the widget (that is, the build function).
Modifying the code:
//other imports here
import 'package:local_auth/local_auth.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//removing the authentication block from the main method
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]).then((_) {
runApp(ProviderScope(child: MyApp()));
});
}
class MyApp extends StatefulWidget { //changing MyApp to StatefulWidget
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
_authenticate(); //the function that handles authentication
}
void _authenticate() async {
var localAuth = LocalAuthentication();
SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.getBool('auth') == true) {
var authenticate = await localAuth.authenticate(
localizedReason: 'Authenticate to access Notes',
useErrorDialogs: true,
//Not using stickyAuth because: https://github.com/flutter/flutter/issues/83773
// stickyAuth: true,
);
if (authenticate != true)
exit(0); //exiting the app if the authentication failed
}
}
#override
Widget build(BuildContext context) {
//returns my widget
}
}
Related
I have an application. I added an introductory screen to this application that only appears at the entrance. This screen will appear when the user first opens the application. The next time they open, the promotional screen will not open again.
I've done it with Secure Storage and GetX whether the user has passed the promotion or not.
main.dart:
import 'package:flutter/material.dart';
import 'package:teen_browser/pages/home.dart';
import 'package:teen_browser/welcome_screens/welcomeMain.dart';
import 'package:get/get.dart';
import 'features/controllers/pagination-controller/pagination_controller.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
var skipStatus;
void main() {
runApp(mainApp());
}
class mainApp extends StatefulWidget {
mainApp({Key? key}) : super(key: key);
#override
State<mainApp> createState() => _mainAppState();
}
class _mainAppState extends State<mainApp> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(fontFamily: 'Montserrat Regular'),
home: passedTheWelcomeScreen(),
);
}
}
class passedTheWelcomeScreen extends StatefulWidget {
passedTheWelcomeScreen({Key? key}) : super(key: key);
#override
State<passedTheWelcomeScreen> createState() => _passedTheWelcomeScreenState();
}
class _passedTheWelcomeScreenState extends State<passedTheWelcomeScreen> {
final PaginationController _paginationController = Get.put(PaginationController());
#override
Widget build(BuildContext context) {
return GetX<PaginationController>(
init: _paginationController,
initState: (initController) {
initController.controller!.CheckSkipStatus();
},
builder: (controller) {
if (controller.isSkipped.value == null) {
return CircularProgressIndicator();
} else if (controller.isSkipped.value == true){
return HomeApp();
} else {
return welcomeMain();
}
},
);
}
}
pagination_controller.dart:
import 'package:get/get.dart';
import 'package:teen_browser/functions/secure_storage.dart';
class PaginationController extends GetxController {
RxnBool isSkipped = RxnBool(false);
void CheckSkipStatus() async {
final resp = await UserSecureStorage.isSkip();
isSkipped.value = resp;
}
}
secure_storage.dart:
import 'dart:developer';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class UserSecureStorage {
static const _storage = FlutterSecureStorage();
static Future setField(String key, value) async {
await _storage.write(key: key, value: value);
}
static Future<String?> getField(key) async {
return await _storage.read(key: key);
}
static Future deleteField(String key) async {
return await _storage.delete(key: key);
}
static Future deleteAll() async {
return await _storage.deleteAll();
}
static Future<bool> isSkip() async {
inspect(await getField("isSkipped"));
var value = await getField('isSkipped');
if (value != null) return true;
inspect(value);
return false;
}
}
On the last promotion page, I stated that the user passed the definition as follows:
await UserSecureStorage.setField("isSkipped", "true");
controller.isSkipped.value = true;
If the user has not passed the promotion before, there is no problem. All promotional screens are coming and error etc. does not give. But after passing all the demo screens, that is, when the value of isSkipped is true, it gives an error.
The error I got:
FlutterError (setState() called after dispose(): _welcomeScreen_1State#7c0ca(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().)
I think the error is caused by the initState on the first demo page, which also shows the first demo page, welcomeMain1.dart, on the console.
welcomeScreen1.dart initState codes:
void initState() {
super.initState();
print(_clickGoButtonErrorMessages[Random().nextInt(_clickGoButtonErrorMessages.length)]);
if (controller.isSkipped.value == false) {
Timer(const Duration(milliseconds: 3200), () {
setState(() {
_helloText = 0;
});
});
Timer(const Duration(milliseconds: 3300), () {
AssetsAudioPlayer.newPlayer().open(
Audio("assets/audios/StopBroDontPassSound.mp3"),
autoStart: true,
showNotification: true,
);
});
Timer(const Duration(milliseconds: 3400), () {
setState(() {
_helloTextState = true;
});
});
Timer(const Duration(milliseconds: 3500), () {
setState(() {
_helloTextState = false;
});
});
Timer(const Duration(milliseconds: 3900), () {
setState(() {
_helloText = 1;
});
});
Timer(const Duration(milliseconds: 4200), () {
setState(() {
_helloTextState = true;
});
});
Timer(const Duration(milliseconds: 5700), () {
setState(() {
_contentText = true;
});
});
Timer(const Duration(milliseconds: 7000), () {
setState(() {
_letsGoButton = true;
});
});
Timer(const Duration(milliseconds: 7300), () {
setState(() {
_skipButton = true;
});
});
Timer(const Duration(milliseconds: 9000), () {
setState(() {
_clickGoButton = true;
_clickSkipButton = true;
});
});
}
else {
inspect("isSkipped true");
}
}
Once the user introduction has passed, it will always be redirected to the homepage, ie HomeApp. If the value of isSkipped is false, it means that the user first opened the application. This time, it directs you to the promotional screens.
My guess is that the application is trying to run the initState codes in welcomeMain1.dart while redirecting to HomeApp() and throws an error when it doesn't.
I hope I was able to explain my problem.When the user promotion passes, the value of isSkipped is true, ie "user promotion passed". I tested it by printing it to the console. How can I solve this problem?
Thanks for help.
There could be a race condition so that isSkipped is false long enough for the timers to start, then it switches to true and the widget is rebuilt but the timers are still running.
You could add this before setState in your timer callbacks:
Timer(const Duration(milliseconds: 3200), () {
if (!mounted) {
return;
}
setState(() {
_helloText = 0;
});
});
You could also save all timers as individual properties and cancel them in the widget's dispose method:
class MyWidget extends StatelessWidget
{
late Timer _timer1;
late Timer _timer2;
void initState() {
super.initState();
if (controller.isSkipped.value == false) {
_timer1 = Timer(const Duration(milliseconds: 3200), () {
setState(() {
_helloText = 0;
});
});
_timer2 = ...
}
// ...
}
#override
void dispose() {
_timer2.cancel();
_timer1.cancel();
super.dispose();
}
}
(The above is dry-coded, sorry for any errors. Storing the timers in an array is left as an exercise to the reader.)
Hello fellow flutter developers,
I have a bug that's been making my life pretty complicated while developing my own app using Flutter. It goes like this:
User opens app
if they're not signed in, they're redirected to USP page.
If they click next, they're redirected to the sign up page.
Sign-up is provided by Firebase and it's anonymous
If sign-up is successful, a Provider should be triggered and a new page is loaded
The bug is that sometimes the user is sent back to the USP page (meaning their user_id is null) despite no exception between thrown during sign-up. If I force the navigation to the signed-in page, then the user doesn't have an user_id and that's an issue for me.
Any one experienced and fixed the same issue? Below you can see how I built my code, maybe this can help?
This is my main file:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SignIn.initializeFirebase();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
getSignInUser() {
return SignIn().user;
}
Widget getMaterialApp() {
return MaterialApp(
title: 'app_title',
home: HomePagePicker(),
onGenerateRoute: RouteGenerator.generateRoute,
debugShowCheckedModeBanner: false,
);
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<MyUser?>.value(value: SignIn().user, initialData: null),
],
child: getMaterialApp(),
);
}
}
class HomePagePicker extends StatefulWidget {
#override
_HomePagePickerState createState() => _HomePagePickerState();
}
class _HomePagePickerState extends State<HomePagePicker> {
#override
Widget build(BuildContext context) {
MyUser? myUser = Provider.of<MyUser?>(context);
if (myUser == null) return IntroScreen(); // this shows the USPs
else {
// this takes you to the signed-in part of the app
return AnotherScreen();
}
}
}
The IntroScreen is a very simple screen with a few USPs and a button to open the registration page. It goes something like
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'login.dart';
class IntroScreen extends StatelessWidget {
static const routeName = '/introScreen';
#override
Widget build(BuildContext context) {
analytics.setScreenName("introScreen");
return Scaffold(
body: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(statusBarColor: ThemeConfig.darkPrimary),
child: Column(...), // show the USPs
floatingActionButton: getFloatingButton(context)
);
}
Widget getFloatingButton(BuildContext buildContext) {
return FloatingActionButton(
backgroundColor: ThemeConfig.primary,
foregroundColor: Colors.white,
child: Icon(Icons.arrow_forward),
onPressed: () {
navigateToScreen(MyLogin.routeName, buildContext, null);
},
);
}
// this is in another file normally but putting it here for completeness
navigateToScreen(String routeName, BuildContext context, Object? arguments) {
Navigator.pushNamed(
context,
routeName,
arguments: arguments
);
}
The important bit in the registration page is this
Future<void> finalizeRegistration(String userName, String userToken) async {
await usersCollection.add({'userName': userName, "userToken": userToken});
}
Future<void> registerUser(String userName) {
return SignIn()
.anonymousSignIn(userName)
.timeout(Duration(seconds: 2))
.then((userToken) {
finalizeRegistration(userName, userToken)
.then((value) => Navigator.pop(context));
})
.catchError((error) {
registrationErrorDialog();
});
}
The SignIn class is the following
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:house_party/models/MyUser.dart';
class SignIn {
final FirebaseAuth _auth = FirebaseAuth.instance;
static Future<FirebaseApp> initializeFirebase() async {
FirebaseApp firebaseApp = await Firebase.initializeApp();
return firebaseApp;
}
Stream<MyUser?> get user {
return _auth
.authStateChanges()
.asyncMap(getUser);
}
Future<String> anonymousSignIn(String userName) async {
var authResult = await _auth.signInAnonymously();
return authResult.user!.uid;
}
Future<MyUser?> getUser(User? user) async {
if (user == null) {
return Future.value(null);
}
return FirebaseFirestore.instance
.collection('users')
.where('userToken', isEqualTo: user.uid)
.get()
.then((res) {
if (res.docs.isNotEmpty) {
return MyUser.fromFireStore(res.docs.first.data());
} else {
return null;
}
});
}
}
Finally, I'm using these versions of firebase
firebase_core: ^1.0.0
cloud_firestore: ^1.0.0
firebase_dynamic_links: ^2.0.0
firebase_auth: 1.1.2
firebase_analytics: ^8.1.1
I hope the problem statement is clear enough!
Thanks in advance!
I fixed this problem (or at least I'm not able to reproduce it anymore) by upgrading the firebase libraries as follows
firebase_core: ^1.4.0
cloud_firestore: ^2.4.0
firebase_dynamic_links: ^2.0.7
firebase_auth: 3.0.1
firebase_analytics: ^8.2.0
I want to open the appropriate window depending on whether the user is authenticated or not.
But now an error appears for a second
[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()
and then the map does not open even though the user is authenticated.
"start" it is a variable that is initialRoute
Here is my code.
void main() => runApp(App());
String start = "";
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
Firebase.initializeApp().whenComplete((){
FirebaseAuth.instance
.authStateChanges()
.listen((User user) {
if (user == null) {
print('User is currently signed out!');
start = '/';
} else {
print('User is signed in!');
start = '/map/${FirebaseAuth.instance.currentUser.uid}';
}
});
});
WidgetsFlutterBinding.ensureInitialized();
return FutureBuilder(
builder: (context, snapshot) {
return MyApp();
}
);
}
}
Initialize your default firebase app like below;
Future<void> initializeDefault() async {
FirebaseApp app = await Firebase.initializeApp();
assert(app != null);
setState(() {
loading = false;
}
);
print('Initialized default app $app');
}
#override
void initState() {
initializeDefault();
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.transparent));
super.initState();
}
I'm new to flutter and trying to create a login app.
I have 2 screens.
Login (If user enters correct credentials, store user information to local db(sqflite) and navigate to home).
Home (have logout option).
I'm trying to achieve auto login i.e when user closes the app without logging out, the app should navigate to home automatically without logging again when app reopens.
My logic:
If user enters valid credentials, clear the db table and insert newly entered credentials.
Auto login - when app starts, check if record count in db table is 1, then navigate to home else login.
Here's the code which I have tried:
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final dbHelper = DatabaseHelper.instance;
bool logged = false;
#override
void initState() {
super.initState();
autoLogIn();
}
void autoLogIn() async {
if (await dbHelper.queryRowCount() == 1) {
setState(() {
logged = true;
});
return;
}
}
#override
Widget build(BuildContext context) {
return logged ? HomeScreen(): LoginScreen();
}
}
It makes me as if, the widget is build before the state of logged is changed.
How can I achieve auto login assuming there is no issue with database(sqflite) implementation.
I have used SharedPreferences instead of local database and that worked.
Below is the minimal implementation
import 'package:IsBuddy/Screens/Dashboard/dashboard.dart';
import 'package:IsBuddy/Screens/Login/login_screen.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AutoLogin extends StatefulWidget {
#override
_AutoLoginState createState() => _AutoLoginState();
}
class _AutoLoginState extends State<AutoLogin> {
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
Future<bool> logged;
#override
void initState() {
logged = _prefs.then((SharedPreferences prefs) {
return (prefs.getBool('logged') ?? false);
});
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: logged,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Container(
child: Center(
child: CircularProgressIndicator(),
));
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return snapshot.data ? Dashboard() : LoginScreen();
}
}
},
);
}
}
I am very new to Flutter and Dart but I am trying to get data from a sqlite method and pass it as an argument to a new widget.
I've tried using FutureBuilder and it didn't help.
I am calling the method to load the data, wait for the data in .then() and inside then I set the state of a class variable to later be assigned to widget but the variable _records is always null even after setState.
Below is my code
int initScreen;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
initScreen = await prefs.getInt("initScreen");
await prefs.setInt("initScreen", 1);
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State{
List<Quarter> _records;
#override
void initState() {
_getRecords();
super.initState();
}
_getRecords() async {
await QuarterData().getQuarters().then((list){
setState((){
this. _records = list;
});
});
return list;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'UI',
debugShowCheckedModeBanner: false,
home: initScreen == 0 || initScreen == null
? SpashScreen(this._records)
: QuartersHome(this._records)
);
}
}
Many thanks
this._records is not initialised and will be null until your query is not done.
You need to check if it's not null before returning your widgets with it (like you do with initScreen)
If after the setState()method your variable is still null, maybe it's your query which return null.