-
-
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; | |
} |
Good example! 👌🏻
Good example! 👌🏻
Thanks
firebase_auth: 0.14.0+7
did not work for me, had to change it to firebase_auth: 0.14.0+5
The 'optional' SHA-1 while registering your app in Firebase is mandatory to fill in, otherwise you get an obscure error
firebase_auth:
0.14.0+7
did not work for me, had to change it tofirebase_auth: 0.14.0+5
The '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?
😎 awesome!