Flutter: firebase authorization flow won't work with routes - flutter

I'm trying to make sure that app-users that aren't authorised (signed in), are directed to the sign in page. This is what my main.dart looks like:
class App extends StatelessWidget {
Widget build(BuildContext context) {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.active) {
// some loading widget
return MaterialApp(home: Scaffold(),);
}
FirebaseUser user = snapshot.data;
if (user == null) {
return MaterialApp(home: SignIn(),);
}
// this is the main app
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/new_game': (context) => NewGame(),
'/join_session': (context) => JoinSession(),
'/my_rankings': (context) => MyRankings(),
'/settings': (context) => Settings(),
},
);
}
);
}
}
When I run the app (on chrome using the web function) I start of at the sign in page (as expected) which just contains an anonymous sign in button. When I sign in it gives an error, stating that the initial-route-builder (route '/' with HomeScreen() as builder) returns null. When I swap my the main app for a simple
return MaterialApp(home: Scaffold(body: Text('This Works')));
it does seem to work. When using a simple MaterialApp() that does the same but using routes, it gives the error again, so the problem seems to be the routing. What's going on?

i don't think it will work like that. the main.dart should be used only as the app entry point.
class App extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => SplashScreen(),
'/home': (context) => HomeScreen(),
'/new_game': (context) => NewGame(),
'/join_session': (context) => JoinSession(),
'/my_rankings': (context) => MyRankings(),
'/settings': (context) => Settings(),
},
);
}
}
class SplashScreen extends StatelessWidget {
void isLogged StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.active) {
// some loading widget
Navigator.pushReplacementNamed(context, '/home');
}
FirebaseUser user = snapshot.data;
if (user == null) {
Navigator.pushReplacementNamed(context, '/SignIn');
}
// this is the main app
);
initiatState(){
isLogged();
}
Widget build(BuildContext context){
}
}
You could use this method to check if a user is logged auth.currentUser() it returns null if the user is not signed
FirebaseAuth auth = FirebaseAuth.instance;
await auth.currentUser() == null ? false : true;

Just do this!
import 'package:base_app/screens/auth/login/index.dart';
import 'package:base_app/screens/boarding/index.dart';
import 'package:base_app/screens/main/home/index.dart';
import 'package:base_app/screens/main/profile/index.dart';
import 'package:base_app/screens/splash/index.dart';
import 'package:base_app/style/palette.dart';
import 'package:base_app/style/theme.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
fontFamily: 'DancingScript',
textTheme: getTextTheme(context),
primarySwatch: colorRed as MaterialColor,
),
home: LandingFlow(),
routes: {
ProfileScreen.id: (context) => ProfileScreen(),
},
);
},
);
}
}
class LandingFlow extends StatefulWidget {
#override
_LandingFlowState createState() => _LandingFlowState();
}
class _LandingFlowState extends State<LandingFlow> {
bool isSplashOver = false;
bool hasBoardingScreensShown = true;
#override
Widget build(BuildContext context) {
if (isSplashOver) {
if (hasBoardingScreensShown) {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasData) {
debugPrint('HomeScreen');
return HomeScreen();
} else {
debugPrint('LoginScreen');
return LoginScreen();
}
},
);
}
return BoardingScreen();
}
return SplashScreen(
onFinished: () => setState(() {
isSplashOver = true;
}),
);
}
}
And for your Splash Screen:
import 'package:flutter/material.dart';
class SplashScreen extends StatelessWidget {
static const id = 'SplashScreen';
final Function onFinished;
const SplashScreen({this.onFinished});
#override
Widget build(BuildContext context) {
Future.delayed(
const Duration(seconds: 3),
() => onFinished(),
);
return const Scaffold(
body: Center(
child: Text('Splash Screen'),
),
);
}
}

Related

Flutter go_router package redirects not works properly

according to this official video from author of go_router package :
Flutter Navigator 2.0 made easy with go_router
and this Flutter Navigator 2.0: Using go_router complete guide , I implemnted go_router in my app. However, I'm having a strange problem. Everything works properly the first time I run my app. I can fetch user information from the server and send them to the home page. However, I'm getting an issue regarding fetching data after restarting my app. Would you please tell me why I'm receiving this error?
this my main.dart :
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPrefs = await SharedPreferences.getInstance();
runApp(MyApp(sharedPrefs));
}
class MyApp extends StatefulWidget {
final SharedPreferences sharedPreferences;
MyApp(this.sharedPreferences);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late LoginState loginState;
late Auth auth;
#override
void initState() {
loginState = LoginState(widget.sharedPreferences);
auth = Auth(widget.sharedPreferences);
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => auth,
),
ChangeNotifierProvider(
create: (ctx) => loginState,
),
ChangeNotifierProvider(
create: (ctx) => AppRouter(loginState),
),
ListenableProxyProvider<Auth, Orders>(
update: (_, authObj, prevOrders) =>
Orders(authObj.usrName, authObj.objId),
),
ListenableProxyProvider<Auth, FavMeals>(
update: (_, authObj, prevOrders) =>
FavMeals(authObj.usrName, authObj.objId),
],
child: Sizer(builder: (context, orientation, deviceType) {
final _router = Provider.of<AppRouter>(context, listen: false).router;
return MaterialApp.router(
routerDelegate: _router.routerDelegate,
routeInformationParser: _router.routeInformationParser,
scaffoldMessengerKey: scaffoldMessengerKey,
theme: ThemeData(
primarySwatch: myMaterialColor),
localizationsDelegates: [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
);
}));
}
}
and here is my router.dart file :
class AppRouter with ChangeNotifier {
final LoginState authState;
AppRouter(this.authState);
late final router = GoRouter(
redirect: (state) {
final loginLoc = state.namedLocation(authScreen);
// 2
final loggingIn = state.subloc == loginLoc;
// 3
// 4
final loggedIn = authState.loggedIn;
final rootLoc = state.namedLocation(root);
// 5
if (!loggedIn && !loggingIn) return loginLoc;
if (loggedIn && (loggingIn)) return rootLoc;
return null;
},
refreshListenable: authState,
routes: [
GoRoute(
name: root,
path: '/',
builder: (context, state) => TabsScreen(),
),
GoRoute(
name: mainScreen,
path: '/main-screen',
builder: (context, state) => HomeScreen(),
),
GoRoute(
name: splashScreen,
path: '/splash-screen',
builder: (context, state) => SplashScreen(),
)
GoRoute(
name: authScreen,
path: '/auth-screen',
builder: (context, state) => AuthScreen(),
),
GoRoute(
name: orderSreen,
path: '/order-screen',
builder: (context, state) => OrderScreen(),
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
}
there is a method in my app within my '/' (root) screen that trying to fetch user data and then load the screen, this method doesnt work properly any more after I migrate to go_router :
#override
void initState() {
_selectedIndex = 0;
_pageController = PageController(initialPage: _selectedIndex!);
initUserStatus().then((_) {
setState(() {
_isloading = false;
});
}, onError: (error) {
GoRouter.of(context).goNamed(noNetwork);
print(error.toString());
});
super.initState();
}
Future<void> initUserStatus() async {
final userProvider = Provider.of<Users>(context, listen: false);
await userProvider.userRetrieving();
await Provider.of<EatenMeals>(context, listen: false).serverList();
var user = userProvider.initialUser;
dailyLimitAlert(user);
if (user.isPremium) {
var sub = await userProvider.subRemainDays();
if (sub < 1) {
await userProvider.changePremiumStatus();
}
} else {
print(user.isPremium);
}
}

Flutter + Hive Check if value is present in box in Future Builder

I am crafting a new app and saving the response from a REST API call to a Hive box, which is successful (as far as I know). What I am trying to do in my main.dart is check if the value for token is set in Hive and if it is load an alternative view other than the login view.
My main.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:myapp/model/user_model.dart';
import 'package:myapp/views/login_view.dart';
import 'package:myapp/views/project_view.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocsDir = await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocsDir.path);
Hive.registerAdapter(UserModelAdapter());
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<PocketPolarix> createState() => _PocketPolarixState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
final user = Hive.box('user');
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2036B8),
foregroundColor: Colors.white,
),
),
home: FutureBuilder(
future: Hive.openBox('user'),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
Hive.openBox('user');
if (user.get('token') != null) {
return const ProjectView();
} else {
return const LoginView();
}
}
} else {
return Scaffold();
}
},
),
);
}
#override
void dispose() {
Hive.close();
super.dispose();
}
The code is failing at the second if statement where I check to see if Hive.box('user').get('token') != null what I keep getting is the following error.
throw HiveError('Box not found. Did you forget to call Hive.openBox()?'); as you can see from the code however, I am opening the box.
I am new to Dart and Flutter and Hive for that matter so a helping hand here would be great, thanks!
Part 1 of the issue is that you were trying to access user before opening. Secondly, I believe you need to check if the snapshot.hasData before proceeding with any operation i.e.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2036B8),
foregroundColor: Colors.white,
),
),
home: FutureBuilder(
future: Hive.openBox<YourUserModelBox>('user'),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else if (snapshot.hasData) {
if (snapshot.data is YourUserModelBox) {
final user = snapshot.data as YourUserModelBox;
if (user.get('token') != null) {
return const ProjectView();
} else {
return const LoginView();
}
} else {
Scaffold();
}
}
} else {
return Scaffold();
}
},
),
);
}
The problem is with the first line of code in your build function. You're trying to get the user box from Hive without opening it first. Here is what can you do instead:
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2036B8),
foregroundColor: Colors.white,
),
),
home: FutureBuilder<Box>(
future: Hive.openBox('user'),
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
if (snapshot.data?.get('token') != null) {
return const ProjectView();
} else {
return const LoginView();
}
}
} else {
return Scaffold();
}
},
),
);
}

Provider is not working when navigate to new screen

I implemented Authentication by provider
The problem is when is the first time myHomeCalss is notified that the user is Authenticated by dont return the correctPage (MainGui)
SplashPages is page with a button continue, and push the login page ,
The Login page is pushed outside of costumer
but when I dont pass in the SplashPages is worked perfectyl
any adea please
//splash page
ContinueButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ListenableProvider.value(
value: yourModel,
child: LoginPage(),
),
),
);
}
)
//main
void main() async {
setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await firebase_core.Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthenticationService()),
],
child: MyApp(),
),
);
}
//My app
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHome(),
builder: (context, child) => Navigator(
key: locator<DialogService>().dialogNavigationKey,
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => DialogManager(child: child)),
));
}
}
MyHome
Class MyHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
child: FutureBuilder<bool>(
future: startTime(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot2) {
if (snapshot2.hasData) {
if (snapshot2.data) {
return SplashPages();
} else {
return Consumer<AuthenticationService>(builder: (_, auth, __) {
if (auth.currentUserr == null) {
return LoginPage();
} else {
return FutureBuilder(
future: auth.populateCurrentUser(auth.currentUserr),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (auth.currentUserr.emailVerified) {
return MainGui();
} else {
return ValidationMailPage(
email: auth.currentUserr.email,
);
}
} else
return Container(
// child: Center(
// child: SpinKitRotatingCircle(
// color: Colors.white,
// size: 50.0,
// ))
);
});
}
});
}
}
You may consider using SharedPreferences, in which you will store the user (or maybe just the token), and then check in main if there is a token/user stored there before rendering the app; if there is a token you log in and then push to the homepage, if not you navigate directly to the login page.
SharedPrefenreces is persisted data storage that persists even if you restart the app, but Provider is a state management solution that doesn't persist between app restarts.
Here is the SharedPreferences plugin you may use.

How to load state on app startup using provider

I am using Provider for app state management in Flutter, Auth data is stored in shared preferences and I want to load it using provider when app starts, what is the best way to do it.
I am going to use auth status to decide whether user should see login screen or Dashboard screen
this is part of a code i used when i started flutter, implement rest of the functions as you wish.
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './providers/authentication.dart';
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => Authentication(),
),
],
child: Consumer<Authentication>(
builder: (ctx, auth, _) => MaterialApp(
title: 'MyApp',
home: auth.isAuthorized
? MyScreen()
: FutureBuilder(
future: auth.tryLogin(),
builder: (ctx, authResultSnapshot) =>
authResultSnapshot.connectionState ==
ConnectionState.waiting
? SplashScreen()
: AuthScreen(),
),
authentication.dart
String _token;
DateTime _expiryDate;
String _userId;
bool get isAuthorized {
// that is a very simple check
return token != null;
}
Future<bool> tryLogin() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey('userData')) {
return false;
}
final extractedUserData = json.decode(prefs.getString('userData')) as Map<String, Object>;
final expiryDate = DateTime.parse(extractedUserData['expiryDate']);
if (expiryDate.isBefore(DateTime.now())) {
return false;
}
_token = extractedUserData['token'];
_userId = extractedUserData['userId'];
_expiryDate = expiryDate;
notifyListeners();
return true;
}
Something like this:
Future<void> main() async {
final appState = await loadAppStateFromSharedData();
runApp(
Provider.value(
value: appState,
child: MyApp(),
),
);
}
Try this one!
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Auth>(
builder: (ctx, auth, _) => MaterialApp(
title: 'AppName',
home: auth.isAuth
? DashboardScreen()
: AuthScreen(),
),
),
};
}

Flutter Firebase Authentication: delay on startup

I am using Provider and the stream FirebaseAuth.instance.onAuthStateChanged in the app to decide where to redirect on startup, but although the user is already logged in (from a previous startup) the app starts on the login screen and almost 1 second later redirects to the home page, from which it should have started from the first moment. This happens even in airplane mode.
I would like to know if there is any approach to solve this, even if it is not possible to show the home screen at once, I don't know how to differentiate between the not logged user (null->login screen) and loading user (null->loading screen).
Some of the code:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
final DatabaseService db = DatabaseService();
#override
Widget build(BuildContext context) {
return StreamProvider<FirebaseUser>.value(
value: _auth.onAuthStateChanged,
child: Consumer<FirebaseUser>(
builder: (context, firebaseUser, child) {
return MultiProvider(
providers: [
if (firebaseUser != null)
ChangeNotifierProvider(create: (ctx) => CollectionState(firebaseUser)),
StreamProvider<List<Collection>>.value(value: db.streamCollections(firebaseUser)),
],
child: MaterialApp(
title: 'My App',
routes: {
'/': (ctx) => LandingPage(),
'/login': (ctx) => LoginPage(),
'/emailSignIn': (ctx) => EmailSignInPage(),
'/emailSignUp': (ctx) => EmailSignUpPage(),
'/emailUnverified': (ctx) => EmailUnverifiedPage(),
'/home': (ctx) => HomePage(),
'/settings': (ctx) => Settings(),
},
),
);
},
),
);
}
}
class LandingPage extends StatelessWidget {
final DatabaseService _db = DatabaseService();
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context);
final userCondition =
user == null ? 'null' : user.isEmailVerified ? 'verifiedUser' : 'unverifiedUser';
switch (userCondition) {
case 'null':
return LoginPage();
break;
case 'unverifiedUser':
return EmailUnverifiedPage();
break;
case 'verifiedUser':
return HomePage();
break;
}
}
}
The code is a bit simplified, I use a service for the authentication instance instead, just that.
I know I'm very late, but I've had the same problem for weeks and I finally figured it out.
#ChinkySight is right when he says it's best to use a StreamBuilder, mostly because you have access to the connectionState property.
The reason why lag exists is because the connection to the stream is not fully established. So during ConnectionState.waiting, return a widget like a splash screen or just a container.
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (_, snapshot) {
// Added this line
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
if (snapshot.data is FirebaseUser && snapshot.data != null) {
return HomePage();
}
return LoginPage();
});
}
}
You can even give your return statements fancy animations with the Animated Switcher
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
Widget widget;
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
switch (snapshot.hasData) {
case (true):
widget = HomePage();
break;
case (false):
widget = LoginPage();
}
return Stack(
children: <Widget>[
Scaffold(
backgroundColor: Colors.grey.shade200,
),
AnimatedSwitcher(
duration: Duration(milliseconds: 700),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
)
],
);
},
);
This works for FlutterFire.
Firebase Auth enables you to subscribe in realtime to this state via a
Stream. Once called, the stream provides an immediate event of the
user's current authentication state, and then provides subsequent
events whenever the authentication state changes. To subscribe to
these changes, call the authStateChanges() method on your FirebaseAuth
instance:
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:flutter/material.dart';
import 'menu.dart';
import 'login.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:firebase_core/firebase_core.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitUp]);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TestApp',
theme: ThemeData(primarySwatch: Colors.blue),
home:
StreamBuilder<auth.User>(
stream: auth.FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<auth.User> snapshot) {
if(snapshot.hasData) {
print("data exists");
return HomePage();
}
else {
return LoginPage();
}
},
)
);
}
}