-
-
Save slightfoot/6f97d6c1ec4eb52ce880c6394adb1386 to your computer and use it in GitHub Desktop.
| /* | |
| This example uses the following packages: | |
| firebase_auth: 0.14.0+5 | |
| google_sign_in: 4.0.7 | |
| provider: 3.1.0+1 | |
| Make sure you have setup your project with Firebase by following these instructions: | |
| 1. Follow Option 1 instructions here up to Step 3 | |
| https://firebase.google.com/docs/android/setup#console | |
| Firebase Console: https://console.firebase.google.com | |
| Be sure to configure your SHA-1 or SHA-256 hash in the | |
| Firebase Project Settings for your app. | |
| If you are wondering what your package-name is you can get it from: | |
| <flutter-project>/android/app/build.gradle labelled applicationId. | |
| 2. Place the downloaded 'google-services.json' file from Step 1 above in your | |
| projects <flutter-project>/android/app/ directory. | |
| 3. Follow the instructions here to enable the Google Services Gradle Plugin. | |
| https://pub.dev/packages/firebase_auth#android-integration | |
| 4. Go to the Firebase Console and then to the Authentication section and then | |
| on to the "Sign-in method" tab an enable Email/Password and Google Sign in methods. | |
| 5. Run and enjoy! ... luckily you only have to do this once. | |
| */ | |
| // MIT License | |
| // | |
| // Copyright (c) 2019 Simon Lightfoot | |
| // | |
| // Permission is hereby granted, free of charge, to any person obtaining a copy | |
| // of this software and associated documentation files (the "Software"), to deal | |
| // in the Software without restriction, including without limitation the rights | |
| // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| // copies of the Software, and to permit persons to whom the Software is | |
| // furnished to do so, subject to the following conditions: | |
| // | |
| // The above copyright notice and this permission notice shall be included in all | |
| // copies or substantial portions of the Software. | |
| // | |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| // SOFTWARE. | |
| // | |
| import 'dart:async'; | |
| import 'package:firebase_auth/firebase_auth.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/services.dart' show PlatformException; | |
| import 'package:google_sign_in/google_sign_in.dart'; | |
| import 'package:provider/provider.dart'; | |
| Future<void> main() async { | |
| WidgetsFlutterBinding.ensureInitialized(); | |
| // Async main methods show the platform splash screen | |
| // before runApp is called to show the flutter UI. | |
| // So this line waits for Auth.create to finish before | |
| // showing the rest of the app. | |
| runApp(App(auth: await Auth.create())); | |
| } | |
| class App extends StatefulWidget { | |
| const App({ | |
| Key key, | |
| @required this.auth, | |
| }) : super(key: key); | |
| final Auth auth; | |
| @override | |
| _AppState createState() => _AppState(); | |
| } | |
| class _AppState extends State<App> { | |
| final _navigatorKey = GlobalKey<NavigatorState>(); | |
| FirebaseUser currentUser; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| currentUser = widget.auth.init(_onUserChanged); | |
| } | |
| void _onUserChanged() { | |
| final user = widget.auth.currentUser.value; | |
| // User logged in | |
| if (currentUser == null && user != null) { | |
| _navigatorKey.currentState.pushAndRemoveUntil(Main.route(), (route) => false); | |
| } | |
| // User logged out | |
| else if (currentUser != null && user == null) { | |
| _navigatorKey.currentState.pushAndRemoveUntil(Login.route(), (route) => false); | |
| } | |
| currentUser = user; | |
| } | |
| @override | |
| void dispose() { | |
| widget.auth.dispose(_onUserChanged); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return MultiProvider( | |
| providers: <SingleChildCloneableWidget>[ | |
| Provider<Auth>.value(value: widget.auth), | |
| ValueListenableProvider<FirebaseUser>.value(value: widget.auth.currentUser), | |
| ], | |
| child: MaterialApp( | |
| title: 'Your App', | |
| theme: ThemeData( | |
| brightness: Brightness.light, | |
| primaryColor: Colors.indigo, | |
| accentColor: Colors.pink, | |
| ), | |
| navigatorKey: _navigatorKey, | |
| home: currentUser == null ? const Login() : const Main(), | |
| ), | |
| ); | |
| } | |
| } | |
| /// Logged out view of the app. | |
| class Login extends StatefulWidget { | |
| static Route<dynamic> route() { | |
| return MaterialPageRoute( | |
| builder: (BuildContext context) => const Login(), | |
| ); | |
| } | |
| const Login({ | |
| Key key, | |
| }) : super(key: key); | |
| @override | |
| _LoginState createState() => _LoginState(); | |
| } | |
| class _LoginState extends State<Login> { | |
| final _formKey = GlobalKey<FormState>(); | |
| final _email = TextEditingController(); | |
| final _password = TextEditingController(); | |
| Future _loginFuture; | |
| void _onLoginWithPasswordPressed() { | |
| if (_formKey.currentState.validate()) { | |
| _formKey.currentState.save(); | |
| setState(() { | |
| _loginFuture = Auth.of(context).loginWithEmailAndPassword(_email.text, _password.text); | |
| }); | |
| } | |
| } | |
| void _onLoginWithGooglePressed() { | |
| setState(() { | |
| _loginFuture = Auth.of(context).loginWithGoogle(); | |
| }); | |
| } | |
| String _validateEmail(String value) { | |
| if (value == null || value.trim().isEmpty) { | |
| return 'Please enter an email address.'; | |
| } else if (value.contains('@') == false) { | |
| return 'Please check your email address is correct.'; | |
| } | |
| return null; | |
| } | |
| String _validatePassword(String value) { | |
| if (value == null || value.trim().isEmpty) { | |
| return 'Please enter your password.'; | |
| } else if (value.length < 6) { | |
| return 'Your password needs to be at least 6 characters.'; | |
| } | |
| return null; | |
| } | |
| @override | |
| void dispose() { | |
| _email.dispose(); | |
| _password.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Theme.of(context).primaryColorLight, | |
| body: SafeArea( | |
| child: SizedBox.expand( | |
| child: Column( | |
| children: <Widget>[ | |
| const SizedBox(height: 16.0), | |
| Text( | |
| 'Login Screen', | |
| style: Theme.of(context).textTheme.title, | |
| textAlign: TextAlign.center, | |
| ), | |
| const SizedBox(height: 36.0), | |
| FutureBuilder( | |
| future: _loginFuture, | |
| builder: (BuildContext context, AsyncSnapshot snapshot) { | |
| if (snapshot.connectionState == ConnectionState.waiting) { | |
| return const Center( | |
| child: CircularProgressIndicator(), | |
| ); | |
| } | |
| return Form( | |
| key: _formKey, | |
| child: Padding( | |
| padding: const EdgeInsets.symmetric(horizontal: 32.0), | |
| child: Column( | |
| children: <Widget>[ | |
| TextFormField( | |
| controller: _email, | |
| decoration: const InputDecoration( | |
| labelText: 'Email', | |
| ), | |
| validator: _validateEmail, | |
| ), | |
| TextFormField( | |
| controller: _password, | |
| obscureText: true, | |
| decoration: const InputDecoration( | |
| labelText: 'Password', | |
| ), | |
| validator: _validatePassword, | |
| ), | |
| const SizedBox(height: 16.0), | |
| RaisedButton( | |
| onPressed: _onLoginWithPasswordPressed, | |
| child: const Text('LOGIN'), | |
| ), | |
| const SizedBox(height: 16.0), | |
| RaisedButton( | |
| onPressed: _onLoginWithGooglePressed, | |
| child: const Text('LOGIN WITH GOOGLE'), | |
| ), | |
| if (snapshot.hasError) | |
| Container( | |
| width: double.infinity, | |
| margin: const EdgeInsets.symmetric(vertical: 36.0), | |
| padding: const EdgeInsets.all(12.0), | |
| decoration: BoxDecoration( | |
| border: Border.all(color: Theme.of(context).errorColor), | |
| borderRadius: const BorderRadius.all(Radius.circular(2.0)), | |
| color: Theme.of(context).errorColor.withOpacity(0.6), | |
| ), | |
| child: Text( | |
| snapshot.error.toString(), | |
| style: TextStyle( | |
| color: Colors.white, | |
| ), | |
| textAlign: TextAlign.center, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| /// Main logged in view of the app. | |
| class Main extends StatefulWidget { | |
| static Route<dynamic> route() { | |
| return MaterialPageRoute( | |
| builder: (BuildContext context) => const Main(), | |
| ); | |
| } | |
| const Main({ | |
| Key key, | |
| }) : super(key: key); | |
| @override | |
| _MainState createState() => _MainState(); | |
| } | |
| class _MainState extends State<Main> { | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Theme.of(context).primaryColorDark, | |
| body: SafeArea( | |
| child: SizedBox.expand( | |
| child: Column( | |
| children: <Widget>[ | |
| const SizedBox(height: 16.0), | |
| Text( | |
| 'Main Screen', | |
| style: Theme.of(context).textTheme.title, | |
| textAlign: TextAlign.center, | |
| ), | |
| const SizedBox(height: 36.0), | |
| const UserAvatar(), | |
| const SizedBox(height: 16.0), | |
| RaisedButton( | |
| onPressed: () { | |
| Auth.of(context).logout(); | |
| }, | |
| child: const Text('LOGOUT'), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| /// Displays the user's image | |
| class UserAvatar extends StatelessWidget { | |
| const UserAvatar({ | |
| Key key, | |
| }) : super(key: key); | |
| @override | |
| Widget build(BuildContext context) { | |
| final user = Provider.of<FirebaseUser>(context, listen: false); | |
| return Material( | |
| elevation: 4.0, | |
| shape: const CircleBorder( | |
| side: BorderSide(color: Colors.blue, width: 6.0), | |
| ), | |
| color: Colors.black, | |
| clipBehavior: Clip.antiAlias, | |
| child: SizedBox( | |
| width: 96.0, | |
| height: 96.0, | |
| child: user?.photoUrl != null | |
| ? Image.network(user.photoUrl) | |
| : Icon( | |
| Icons.person, | |
| color: Colors.white, | |
| size: 72.0, | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| /// Auth backend | |
| class Auth { | |
| static Future<Auth> create() async { | |
| final currentUser = await FirebaseAuth.instance.currentUser(); | |
| return Auth._(currentUser); | |
| } | |
| static Auth of(BuildContext context) { | |
| return Provider.of<Auth>(context, listen: false); | |
| } | |
| Auth._( | |
| FirebaseUser currentUser, | |
| ) : this.currentUser = ValueNotifier<FirebaseUser>(currentUser); | |
| final ValueNotifier<FirebaseUser> currentUser; | |
| final _googleSignIn = GoogleSignIn(); | |
| final _firebaseAuth = FirebaseAuth.instance; | |
| StreamSubscription<FirebaseUser> _authSub; | |
| FirebaseUser init(VoidCallback onUserChanged) { | |
| currentUser.addListener(onUserChanged); | |
| _authSub = _firebaseAuth.onAuthStateChanged.listen((FirebaseUser user) { | |
| currentUser.value = user; | |
| }); | |
| return currentUser.value; | |
| } | |
| void dispose(VoidCallback onUserChanged) { | |
| currentUser.removeListener(onUserChanged); | |
| _authSub.cancel(); | |
| } | |
| Future<void> loginWithEmailAndPassword(String email, String password) async { | |
| try { | |
| await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password); | |
| } catch (e, st) { | |
| throw _getAuthException(e, st); | |
| } | |
| } | |
| Future<void> loginWithGoogle() async { | |
| try { | |
| final account = await _googleSignIn.signIn(); | |
| if (account == null) { | |
| throw AuthException.cancelled; | |
| } | |
| final auth = await account.authentication; | |
| await _firebaseAuth.signInWithCredential( | |
| GoogleAuthProvider.getCredential(idToken: auth.idToken, accessToken: auth.accessToken), | |
| ); | |
| } catch (e, st) { | |
| throw _getAuthException(e, st); | |
| } | |
| } | |
| Future<void> logout() async { | |
| try { | |
| await _firebaseAuth.signOut(); | |
| await _googleSignIn.signOut(); | |
| } catch (e, st) { | |
| FlutterError.reportError(FlutterErrorDetails(exception: e, stack: st)); | |
| } | |
| } | |
| AuthException _getAuthException(dynamic e, StackTrace st) { | |
| if (e is AuthException) { | |
| return e; | |
| } | |
| FlutterError.reportError(FlutterErrorDetails(exception: e, stack: st)); | |
| if (e is PlatformException) { | |
| switch (e.code) { | |
| case 'ERROR_INVALID_EMAIL': | |
| throw const AuthException('Please check your email address.'); | |
| case 'ERROR_WRONG_PASSWORD': | |
| throw const AuthException('Please check your password.'); | |
| case 'ERROR_USER_NOT_FOUND': | |
| throw const AuthException('User not found. Is that the correct email address?'); | |
| case 'ERROR_USER_DISABLED': | |
| throw const AuthException('Your account has been disabled. Please contact support'); | |
| case 'ERROR_TOO_MANY_REQUESTS': | |
| throw const AuthException('You have tried to login too many times. Please try again later.'); | |
| } | |
| } | |
| throw const AuthException('Sorry, an error occurred. Please try again.'); | |
| } | |
| } | |
| class AuthException implements Exception { | |
| static const cancelled = AuthException('cancelled'); | |
| const AuthException(this.message); | |
| final String message; | |
| @override | |
| String toString() => message; | |
| } |
firebase_auth:
0.14.0+7did not work for me, had to change it tofirebase_auth: 0.14.0+5The 'optional' SHA-1 while registering your app in Firebase is mandatory to fill in, otherwise you get an obscure error
What error did you get with firebase_auth: 0.14.0+7? I did mention the SHA-1 hash. https://gist.github.com/slightfoot/6f97d6c1ec4eb52ce880c6394adb1386#file-main-dart-L15
Ow sorry, I skimmed it to fast, missed the SHA-1 part.
Because <my app name> depends on firebase_auth 0.14.0+7 which doesn't match any versions, version solving failed. pub get failed (1)
By the way, thanks a lot for this example! I've implemented it before in Android and Flutter, but this standalone example is very handy for implementing it again in a new app!
Ow sorry, I skimmed it to fast, missed the SHA-1 part.
Because <my app name> depends on firebase_auth 0.14.0+7 which doesn't match any versions, version solving failed. pub get failed (1)By the way, thanks a lot for this example! I've implemented it before in Android and Flutter, but this standalone example is very handy for implementing it again in a new app!
Thanks.. fixed it. I actually was using my own fixed branch of firebase_auth which is why the version was different.
https://github.com/slightfoot/flutterfire/blob/auth_reauthenticate_fix/packages/firebase_auth/pubspec.yaml
Nice example!
Simon, is it the way you code for your production ready apps? Or this is only for example? Can you please write a post for the way to write code for scalable apps?
firebase_auth:
0.14.0+7did not work for me, had to change it tofirebase_auth: 0.14.0+5The 'optional' SHA-1 while registering your app in Firebase is mandatory to fill in, otherwise you get an obscure error