Flutter Firebase Authentication - delay on app startup/refresh - firebase

I am running an app that has a 'landing page', Basically, when the user opens/restarts/refreshes the app, the landing screen decides whether to direct the user to the sign-in screen (if the user is not logged in) or the home screen (if they are already logged in).
The code I am using for this functionality (involving a StreamBuilder) is this:
class LandingPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final auth = Provider.of<AuthBase>(context, listen: false);
return StreamBuilder<User?>(
stream: auth.authStateChanges(),
builder: (context, snapshot) {
/// If the connection state is waiting, show splash screen or loading icon
if (snapshot.connectionState == ConnectionState.waiting) {
print('Connection waiting');
return Scaffold(
body: Center(
child: PlatformCircularProgressIndicator(),
),
);
}
/// Once the connection is established, then show the disclaimer acceptance or sign-in screen (depending if there is a logged in user or not)
if (snapshot.connectionState == ConnectionState.active) {
User? user = snapshot.data;
return FutureBuilder<IdTokenResult>(
future: user?.getIdTokenResult(true),
builder: (
BuildContext context,
AsyncSnapshot<IdTokenResult> snapshotData,
) {
if (user == null ||
user.emailVerified == false ||
snapshotData.data?.claims!['status'] != 'active') {
return SignInHomePage.create(context);
} else {
return Provider<User?>.value(
value: user,
child: DisclaimerAcceptanceHomeScreen(
userName: user.displayName,
),
);
}
},
);
} else {
return Scaffold(
body: Center(
child: PlatformCircularProgressIndicator(),
),
);
}
});
}
}
The reason I am using a FutureBuilder is that my app is subscription based and I am using cloud functions and custom claims to check to see if a user has an active subscription before allowing them to log in.
My issue is that when a user refreshes/reloads the app, if they are logged in, there is a brief flash where the sign-in screen is shown (can't be more than a second) before the other screen is shown. That comes across as quite poor from a user impression point of view, so I am trying to fix that. Basically, if a user is logged in, I do not want them to be shown even a flash of the sign in screen. I would like it to go straight from the splash screen to the disclaimer home screen that you see.
I came across this answer in which someone appeared to have the same issue as me. They explained that I need to do something when the connection state is waiting, which is why you see the code in there.
Unfortunately, that isn't working for me. For me, the flash of the sign in screen happens when the connection state is active, not when it is waiting. I am not too sure how to correct this.
Does anyone have thoughts about how I could go about fixing this, so that there is not a flash of the sign-in screen at all?

You might use firestore exists on snapshots. I think Signin page shows because when user found with emailVerified false and after that claims value comes, which redirects to landing page. So I'd do something like this:
// make sure snapshot completed
if (snapshotData.exists) {
// if user null or user found but status not verified
if (user == null) {
return SignInHomePage.create(context);
} else {
if (snapshotData.data?.claims!['status'] != 'active') {
return SignInHomePage.create(context);
} else {
return Provider<User?>.value(
value: user,
child: DisclaimerAcceptanceHomeScreen(
userName: user.displayName,
),
);
}
}
} else {
// show loading indicator or something
}

Related

How can I use several providers for re-rendering a widget?

I'm building a Flutter app with Firebase and Riverpod.
Until the main page is displayed a user has to perform several steps to get there (e.g. sign in, validate email, get activated by admin, upload documents). For each of these steps i show a specific page or widget which is determined in AppRouterWidget (see below).
The problem i have is that i need at least 2 different providers to cover all possible states, since some aspects belong to the Firebase user in Authentication area and the others to the user account in Firebase's database (collection 'account'), which is of course only available if the user has logged in.
I can cover the authentication part, but i have no clue how i can add the user account part, which should be accessible by watching accountStreamProvider.
This is what i currently have working:
final accountStreamProvider = StreamProvider((ref) {
final database = ref.watch(databaseProvider)!;
return database.accountStream();
});
class AppRouterWidget extends ConsumerWidget {
const AppRouterWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final authStateChanges = ref.watch(authStateChangesProvider);
return authStateChanges.when (
data: (user) => _data(context, user, ref),
loading: () => const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
error: (_, __) => const Scaffold(
body: Center(
child: Text('Error'),
),
),
);
}
Widget _data(BuildContext context, User? user, WidgetRef ref) {
// user is the auth user
if (user == null) {
// either login or signup
return const AuthPage();
} else {
// logged in. now check which step we have to show
if (!user.emailVerified) {
return const VerifyEmailPage();
} else {
// these are the account specific data
//final accountAsyncValue = ref.watch(accountStreamProvider);
//if (accountAsyncValue.hasValue || !accountAsyncValue.value!.isActiv) {
// return const WeCallYouWidget();
//}
}
return Container();
}
}
}
I guess that i need 2 listeners in build() and both would call _data when triggered, but i don't know exactly how to do this.
Thanks a lot for some insights.
I'd be tempted to move all the logic for deciding which page to show outside your widget.
One way to do this would be to create a StateNotifier<PageState> subclass (PageState could be a Freezed class or an enumeration) that takes all the repositories/data sources you need as arguments, subscribes to all the streams as needed, and computes the output state that the widget can watch as:
final pageState = ref.watch(pageStateProvider);
return pageState.when(
auth: () => ....
verifyEmail: () => ...
uploadDocuments: () => ...
// and so on
);
As a result i took bizz84 advice and moved all logic into a separate class which now holds all needed listeners.
Whenever a new event happens it can react to that notification and determine the new page state which will be used to show the correct page.

Flutter [firebase_auth] onAuthStateChanged stoped listening

onAuthStateChanged no longer listens to changes, so basically, when I create a new account or sign in to an existing one, it's stuck in the loading spinner but the user did create it in the firebase users.
I'm using StreamBuilder:
home:StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (ctx, userSnapshot) {
if (userSnapshot.hasData) {
return SellerScreen(docsRef);
}
return AuthScreen();
},
),
what I get in the console:
D/FirebaseAuth( 9477): Notifying id token listeners about user ( VQghDgOucFdCcWLdPlCJNTd5eO72 ).
D/FirebaseAuth( 9477): Notifying auth state listeners about user ( VQghDgOucFdCcWLdPlCJNTd5eO72 ).
The code in the console stipulates that the user has either been created or signed in.
Your problem might be with where your spinner widget is located in the code...
Can i have a snippet on that to see if I can help?
...in any case, what i usually do is:
bool loading = false;
#override
Widget build(BuildContext context) {
return loading ? Loading() : HomeScreen();
}
With this, I've told flutter to show the loading screen while the user is being created or signed in.
The bool type loading is just to toggle between the true and false state of the loading widget

How can I load the previous signed in user?

In my application, I happen to have two types of users, hospital, and patients, both are configured to use the same authentication by firebase. What I would like to know is, how do I load the page for the respective user depending on how they were signed in previously? For example, if a hospital had signed in and not logged out, when they run the app, it should display the hospitals' dashboard and not the patients'. How do I configure this?
shared_prefs supports Flutter web. Save a value stating the account type.
See this answer:
https://stackoverflow.com/a/59579821/13714686
SharedPreferences prefs = await SharedPreferences.getInstance();
bool isHospital = (prefs.getBool('isHospitalAccount');
if(isHospital == true){
//hospital navigation
}
else{
//patient navigation
}
First, you need to check whether the user is logged in or not. For this, you can check through the below code.
class MyApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot){
if (snapshot.hasData){
FirebaseUser user = snapshot.data; // this is your user
/// is because there is a user already logged, So route them to a Screen of Dashboard directly.
return MainScreen();
}
/// other way there is no user logged.
return LoginScreen();
}
);
}
}
Now, you need to maintain a list of users in FireStore where the user's metadata like the last login, User Type information will be stored. Using this information you can Route the user accordingly.

How to have flutter show different screens based on whether or not firestore documents exist?

I am pretty new to flutter and are designing an app that requires user to login. Once users have registered an account they will be required to fill out some user information before being taken to the home screen. If the user registers an account however doesnt fill out their info the next time they log in they should be returned to this screen. Currently, there is a stream that watches the value of the user. If this value is null then it shows the login page, otherwise it will show the home page.
I am having trouble implementing the user information screen. Would setting up a stream that watches a firestore document and, if the document exists return the home page, otherwise show the user infromation screen be the best method and if so how would I go about doing so?
Here is my code that checks whether or not the user is signed in:
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(
home: SignedIn(),
)
);
}
And the code for the SignedIn() widget:
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user == null) {
return Authenticate();
} else {
return Home();
}
}
I would probably try something like this.
StreamSubscription _subscription;
_subscription = yourStream.listen((data) {
// navigate to whatever screen depending on the data
if(data.signedIn){
Navigator.pushNamed(context, "whatever_route");
}
});

Listening to changes in firebase auth credentials

I was building an app which requires me to sign in the user with two compulsory methods, the email as well as the phone number as both the type of data is crucial for the proper functioning of the app. I have implemented Firebase email-password based authentication and google sign in into my app along with phone verification.
Now, I have successfully implemented both the methods and managed to link the credentials of phone auth with email and the same is visible on the firebase console but the problem is that I'm not notified of any changes in auth credentials.
Here's the code I've used to determine which page to show depending on the state of snapshot.data
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} else {
if (snapshot.hasError) {
return Scaffold(
body: Center(
child: Text("Error signing in. Please try later"),
),
);
} else if (snapshot.hasData) {
print("signInHandler.dart phone: ${snapshot.data.phoneNumber}");
if (snapshot.data.phoneNumber == null) {
return PhoneSignIn();
}
return MyHomePage();
}
return EmailSignIn();
}
},
);
Now I know as per documentaions, onAuthStateChanged is fired only if there was any sign in or sign out event but is there any way to know that the AuthCredentials have changed so that I can decide the appropriate page to show depending on the data?
However, when I hot reload the app after linking auth credentials, the appropriate screen is displayed i.e MyHomePage(). Before that, the app keeps displaying only the PhoneSignIn() screen ever after successfully linking the credentials.
Getting phone number using google's contacts API did not solve my problem as there are some google accounts which are not associated with any phone number.
I'd be very grateful if someone could point me in the right direction.
singh, you'll need to force refresh the user idToken, getIdToken(true), so as propagate the new values for credentials, claims or providers on the auth user instance. You can read here for more.
In the end, I could not find any out of the box solution for the problem and had to use a bunch of streams and Bloc for managing this state.

Resources