简体   繁体   中英

Firebase auth not persisting on iOS or Android in Flutter

To start, I have gone through more than 20 different questions and solutions here on Stack Overflow about this topic (most of them are related to the web version), I have also tried twitter, and even the FlutterDev Discord server and cannot seem to find this issue.

I am using firebase for mobile authentication for my app, and no matter what I try, I cannot seem to get the persistent auth state to work on iOS or Android.

Here is my main:

Future<void> main() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    runApp(
        MultiProvider(
            ...
        child: const MyApp(),
        ),
    );
}

class MyApp extends StatelessWidget {
    const MyApp({Key? key}) : super(key: key);
    final const ColorScheme colorScheme = ColorScheme(
        ...
    );
    
    @override
    Widget build(BuildContext context) {
        bool isDebug = false;
        if (Constants.DEBUG_BANNER == 'true') {
            isDebug = true;
        }
        return MaterialApp(
            theme: ThemeData(
                ...
            ),
            routes: {
               // This is a general layout of how all my routes are in case this is the issue
               Screen.route: (BuildContext context) => const Screen(),
            },
            home: const HomeScreen(),
            debugShowCheckModeBanner: isDebug,
        );
    }
}

the ... is just code that I think is unrelated to my question and so I am hiding it for brevity. Mostly themes, and private data

Let's just start with my google-sign-in-button and if necessary I can share others if it is important. We are using Facebook, Google, and Apple for iOS.

class GoogleSignInButton extends StatefulWidget {
  const GoogleSignInButton({Key? key}) : super(key: key);

  @override
  _GoogleSignInButtonState createState() => _GoogleSignInButtonState();
}

class _GoogleSignInButtonState extends State<GoogleSignInButton> {
  bool _isSigningIn = false;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 16.0),
      child: _isSigningIn
          ? CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation<Color>(MRRM.colorScheme.primary),
            )
          : OutlinedButton(
              key: const Key('google_sign_in_button'),
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all(Colors.white),
                shape: MaterialStateProperty.all(
                  RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(40),
                  ),
                ),
              ),
              onPressed: () async {
                setState(() {
                  _isSigningIn = true;
                });

                context.read<Member>().signInWithGoogle(context: context).then<void>((void user) {
                  setState(() {
                    _isSigningIn = false;
                  });

                  Navigator.pushReplacementNamed(context, UserInfoScreen.route);
                });
              },
              child: Padding(
                padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    const Image(
                      image: AssetImage('assets/images/png/google_logo.png'),
                      height: 35.0,
                    ),
                    Padding(
                        padding: const EdgeInsets.only(left: 10),
                        child: Text(
                          'Sign in with Google',
                          style: TextStyle(
                            fontSize: 20,
                            color: MRRM.colorScheme.secondary,
                            fontWeight: FontWeight.w600,
                          ),
                        ))
                  ],
                ),
              ),
            ),
    );
  }
}

I am using the provider pub, which is what context.read<Object?>() is from.

Here is the signInWithGoogle function;

Future<String> signInWithGoogle({required BuildContext context}) async {
    final FirebaseAuth _auth = FirebaseAuth.instance;

    final GoogleSignIn googleSignIn = GoogleSignIn();

    final GoogleSignInAccount? googleSignInAccount =
        await googleSignIn.signIn();

    if (googleSignInAccount != null) {
      final GoogleSignInAuthentication googleSignInAuthentication =
          await googleSignInAccount.authentication;

      final AuthCredential credential = GoogleAuthProvider.credential(
        accessToken: googleSignInAuthentication.accessToken,
        idToken: googleSignInAuthentication.idToken,
      );

      try {
        final UserCredential userCredential =
            await _auth.signInWithCredential(credential);

        _firebaseUser = userCredential.user!;
        _authType = AuthType.Google;
        _uuId = _firebaseUser.uid;
        notifyListeners();
      } on FirebaseAuthException catch (e) {
        if (e.code == 'account-exists-with-different-credential') {
          ScaffoldMessenger.of(context).showSnackBar(
            customSnackBar(
              content: 'The account already exists with different credentials.',
            ),
          );
        } else if (e.code == 'invalid-credential') {
          ScaffoldMessenger.of(context).showSnackBar(
            customSnackBar(
              content: 'Error occurred while accessing credentials. Try again.',
            ),
          );
        }
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          customSnackBar(
            content: 'Error occurred using Google Sign-In. Try again.',
          ),
        );
      }
    }
    return getMemberLogin();
  }

This is contained in my Member object, which just stores all of the Auth data as well as the Member specific data that comes from one of our internal API's, and the member data is stored as an App State object in provider, which is linked in the main.dart file

The getMemberLogin() function is just taking the UUID from the auth and sending it to an API and getting internal member data, I would hope that a simple post request isn't what is causing this. but if you think it might let me know and I will try to post it while obfuscating any NDA related data.

This is the home/splash Screen that handles the initial routing and goes to the loadingScreen that is supposed to be checking if there is a persisted login and going to the UserInfo screen instead of the Auth Screen.

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);
  static const String route = '/home';

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        key: const Key('Home'),
        children: <Widget>[
          Expanded(
            child: Image.asset('assets/images/png/Retail_Rebel_Primary.png'),
          ),
          BlinkingTextButton(
            key: const Key('blinking_text_button'),
            textButton: TextButton(
              child: Text(
                'Tap to continue',
                style: TextStyle(
                  color: MRRM.colorScheme.primary,
                  fontSize: 16.0,
                ),
              ),
              onPressed: () {
                Navigator.of(context).pushReplacementNamed(LoadingScreen.route);
              },
            ),
          ),
          Container(
            height: 8.0,
          ),
        ],
      ),
    );
  }
}

And lastly, this is the LoadingScreen that the HomeScreen navigates to:

class LoadingScreen extends StatelessWidget {
  const LoadingScreen({Key? key}) : super(key: key);
  static const String route = '/loadingScreen';
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          if (snapshot.hasData) {
            print('user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) {
              Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
            });
            return const Text('');
          } else {
            print('no user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) {
              Navigator.of(context).pushReplacementNamed(AuthScreen.route);
            });
            return const Text('');
          }
        }
        return const SplashScreen();
      },
    );
  }
}

Not sure if possibly the way that I am handing routing may be the issue, but it is very common for me to use Navigator.of(context).pushReplacementNamed(); unless popping is necessary then I will typically just use Navigator.of(context).pop(); . I usually only use .pop() for modals/alertDialogs, and for things like QR scanners to return to previous screen.

Sorry if this is too much info, or I forgot a ton of stuff. I have been working on trying to get this fixed for a little over a week now and am kind of getting frustrated.

Thank you for any and all responses.

Just because I think it is important to see what I have looked at already, here is a list of a couple of other questions I have looked through that did not help.

This one I believe is dated as of August 2020, especially considering that onAuthStateChanges has been changed to a stream authStateChanges() .

I have also tried just implementing auth in the exact way described in the docs here but same issue.

I also tried just using:

FirebaseAuth.instance.authStateChanges().then((User? user) {
    if (user != null) {
       Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
    } else {
       Navigator.of(context).pushReplacementNamed(AuthScreen.route);
    }

Which didn't work. I have also attempted to just simply check if there is a current user with:

User user = FirebaseAuth.instance.currentUser;
if (user != null && user.uid != null) {
    Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
} else {
    Navigator.of(context).pushReplacementNamed(AuthScreen.route);
}

which still always went to AuthScreen I have also tried all of these methods as asynchronous tasks to see if maybe it is just taking a second to load, and same issue. The weirdest one is with the current method if I take out the if(snapshot.connectionState == ConnectionState.waiting) from the LoadingScreen it will print out no user is logged in immediately followed by user is logged in and then no user is logged in again and then it will navigate to AuthScreen

If you follow what I have done up above, and make a single change, it will work with persisted logins.

change:

class LoadingScreen extends StatelessWidget {
  const LoadingScreen({Key? key}) : super(key: key);
  static const String route = '/loadingScreen';
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          if (snapshot.hasData) {
            print('user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) {
              Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
            });
            return const Text('');
          } else {
            print('no user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) {
              Navigator.of(context).pushReplacementNamed(AuthScreen.route);
            });
            return const Text('');
          }
        }
        return const SplashScreen();
      },
    );
  }
}

to

class LoadingScreen extends StatelessWidget {
  const LoadingScreen({Key? key}) : super(key: key);
  static const String route = '/loadingScreen';
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
        if (snapshot.connectionState == ConnectionState.active) {
          if (snapshot.hasData) {
            print('user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) {
              Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
            });
            return const Text('');
          } else {
            print('no user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) {
              Navigator.of(context).pushReplacementNamed(AuthScreen.route);
            });
            return const Text('');
          }
        }
        return const SplashScreen();
      },
    );
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM