Last active
June 1, 2024 11:10
-
-
Save slightfoot/6f97d6c1ec4eb52ce880c6394adb1386 to your computer and use it in GitHub Desktop.
Firebase Login/Logout Example - by Simon Lightfoot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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?