[web_dashboard] add logout (#447)

* logout wip

* Use AnimatedSwitcher, change lingo from "login" to signIn"

* add automatic sign-in

* fix flashing sign in button

* sign out of FirebaseAuth and GoogleSignIn

* formatting

* change isSignedIn() to getter

* Add error handling for sign in

* improve error handling at login screen
pull/456/head
John Ryan 5 years ago committed by GitHub
parent 46a3f2dd09
commit 8a9bcfa113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,7 +23,9 @@ class AppState {
AppState(this.auth); AppState(this.auth);
} }
/// Creates a [DashboardApi] when the user is logged in. /// Creates a [DashboardApi] for the given user. This allows users of this
/// widget to specify whether [MockDashboardApi] or [ApiBuilder] should be
/// created when the user logs in.
typedef DashboardApi ApiBuilder(User user); typedef DashboardApi ApiBuilder(User user);
/// An app that displays a personalized dashboard. /// An app that displays a personalized dashboard.
@ -63,41 +65,62 @@ class _DashboardAppState extends State<DashboardApp> {
return Provider.value( return Provider.value(
value: _appState, value: _appState,
child: MaterialApp( child: MaterialApp(
home: Builder( home: SignInSwitcher(
builder: (context) => SignInPage( appState: _appState,
auth: _appState.auth, apiBuilder: widget.apiBuilder,
onSuccess: (user) => _handleSignIn(user, context, _appState),
),
), ),
), ),
); );
} }
}
/// Switches between showing the [SignInPage] or [HomePage], depending on
/// whether or not the user is signed in.
class SignInSwitcher extends StatefulWidget {
final AppState appState;
final ApiBuilder apiBuilder;
SignInSwitcher({
this.appState,
this.apiBuilder,
});
@override
_SignInSwitcherState createState() => _SignInSwitcherState();
}
/// Sets the DashboardApi on AppState and navigates to the home page. class _SignInSwitcherState extends State<SignInSwitcher> {
void _handleSignIn(User user, BuildContext context, AppState appState) { bool _isSignedIn = false;
appState.api = widget.apiBuilder(user);
_showPage(HomePage(), context); @override
Widget build(BuildContext context) {
return AnimatedSwitcher(
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeOut,
duration: Duration(milliseconds: 200),
child: _isSignedIn
? HomePage(
onSignOut: _handleSignOut,
)
: SignInPage(
auth: widget.appState.auth,
onSuccess: _handleSignIn,
),
);
} }
/// Navigates to the home page using a fade transition. void _handleSignIn(User user) {
void _showPage(Widget page, BuildContext context) { widget.appState.api = widget.apiBuilder(user);
var route = _fadeRoute(page);
Navigator.of(context).pushReplacement(route); setState(() {
_isSignedIn = true;
});
} }
/// Creates a [Route] that shows [newPage] using a fade transition. Future _handleSignOut() async {
Route<FadeTransition> _fadeRoute(Widget newPage) { await widget.appState.auth.signOut();
return PageRouteBuilder<FadeTransition>( setState(() {
pageBuilder: (context, animation, secondaryAnimation) { _isSignedIn = false;
return newPage; });
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation.drive(CurveTween(curve: Curves.ease)),
child: child,
);
},
);
} }
} }

@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file. // BSD-style license that can be found in the LICENSE file.
abstract class Auth { abstract class Auth {
Future<bool> get isSignedIn;
Future<User> signIn(); Future<User> signIn();
Future signOut(); Future signOut();
} }
@ -10,3 +11,5 @@ abstract class Auth {
abstract class User { abstract class User {
String get uid; String get uid;
} }
class SignInException implements Exception {}

@ -2,6 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a // for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file. // BSD-style license that can be found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart' hide FirebaseUser; import 'package:firebase_auth/firebase_auth.dart' hide FirebaseUser;
@ -11,9 +12,20 @@ class FirebaseAuthService implements Auth {
final GoogleSignIn _googleSignIn = GoogleSignIn(); final GoogleSignIn _googleSignIn = GoogleSignIn();
final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseAuth _auth = FirebaseAuth.instance;
Future<bool> get isSignedIn => _googleSignIn.isSignedIn();
Future<User> signIn() async { Future<User> signIn() async {
try {
return await _signIn();
} on PlatformException catch (e) {
print('Unable to sign in: $e');
throw SignInException();
}
}
Future<User> _signIn() async {
GoogleSignInAccount googleUser; GoogleSignInAccount googleUser;
if (await _googleSignIn.isSignedIn()) { if (await isSignedIn) {
googleUser = await _googleSignIn.signInSilently(); googleUser = await _googleSignIn.signInSilently();
} else { } else {
googleUser = await _googleSignIn.signIn(); googleUser = await _googleSignIn.signIn();
@ -30,7 +42,10 @@ class FirebaseAuthService implements Auth {
} }
Future<void> signOut() async { Future<void> signOut() async {
await _auth.signOut(); await Future.wait([
_auth.signOut(),
_googleSignIn.signOut(),
]);
} }
} }
@ -39,3 +54,4 @@ class _FirebaseUser implements User {
_FirebaseUser(this.uid); _FirebaseUser(this.uid);
} }

@ -2,11 +2,20 @@
// for details. All rights reserved. Use of this source code is governed by a // for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file. // BSD-style license that can be found in the LICENSE file.
import 'dart:math';
import 'auth.dart'; import 'auth.dart';
class MockAuthService implements Auth { class MockAuthService implements Auth {
Future<bool> get isSignedIn async => false;
@override @override
Future<User> signIn() async { Future<User> signIn() async {
// Sign in will randomly fail 25% of the time.
var random = Random();
if (random.nextInt(4) == 0) {
throw SignInException();
}
return MockUser(); return MockUser();
} }

@ -10,6 +10,12 @@ import 'dashboard.dart';
import 'entries.dart'; import 'entries.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
final VoidCallback onSignOut;
HomePage({
@required this.onSignOut,
});
@override @override
_HomePageState createState() => _HomePageState(); _HomePageState createState() => _HomePageState();
} }
@ -20,6 +26,17 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptiveScaffold( return AdaptiveScaffold(
title: Text('Dashboard App'),
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: FlatButton(
textColor: Colors.white,
onPressed: () => _handleSignOut(),
child: Text('Sign Out'),
),
)
],
currentIndex: _pageIndex, currentIndex: _pageIndex,
destinations: [ destinations: [
AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home), AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home),
@ -67,7 +84,36 @@ class _HomePageState extends State<HomePage> {
} }
} }
Widget _pageAtIndex(int index) { Future<void> _handleSignOut() async {
var shouldSignOut = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Are you sure you want to sign out?'),
actions: [
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
),
);
if (!shouldSignOut) {
return;
}
widget.onSignOut();
}
static Widget _pageAtIndex(int index) {
if (index == 0) { if (index == 0) {
return DashboardPage(); return DashboardPage();
} }

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import '../auth/auth.dart'; import '../auth/auth.dart';
class SignInPage extends StatefulWidget { class SignInPage extends StatelessWidget {
final Auth auth; final Auth auth;
final ValueChanged<User> onSuccess; final ValueChanged<User> onSuccess;
@ -16,30 +16,88 @@ class SignInPage extends StatefulWidget {
}); });
@override @override
_SignInPageState createState() => _SignInPageState(); Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SignInButton(auth: auth, onSuccess: onSuccess),
),
);
}
}
class SignInButton extends StatefulWidget {
final Auth auth;
final ValueChanged<User> onSuccess;
SignInButton({
@required this.auth,
@required this.onSuccess,
});
@override
_SignInButtonState createState() => _SignInButtonState();
} }
class _SignInPageState extends State<SignInPage> { class _SignInButtonState extends State<SignInButton> {
Future<bool> _checkSignInFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_checkSignInFuture = _checkIfSignedIn();
} }
@override // Check if the user is signed in. If the user is already signed in (for
Widget build(BuildContext context) { // example, if they signed in and refreshed the page), invoke the `onSuccess`
return Scaffold( // callback right away.
body: Center( Future<bool> _checkIfSignedIn() async {
child: RaisedButton( var alreadySignedIn = await widget.auth.isSignedIn;
child: Text('Sign In'), if (alreadySignedIn) {
onPressed: () async { var user = await widget.auth.signIn();
widget.onSuccess(user);
}
return alreadySignedIn;
}
Future<void> _signIn() async {
try {
var user = await widget.auth.signIn(); var user = await widget.auth.signIn();
if (user != null) {
widget.onSuccess(user); widget.onSuccess(user);
} else { } on SignInException {
throw ('Unable to sign in'); _showError();
}
} }
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _checkSignInFuture,
builder: (context, snapshot) {
// If signed in, or the future is incomplete, show a circular
// progress indicator.
var alreadySignedIn = snapshot.data;
if (snapshot.connectionState != ConnectionState.done ||
alreadySignedIn == true) {
return CircularProgressIndicator();
}
// If sign in failed, show toast and the login button
if (snapshot.hasError) {
_showError();
}
return RaisedButton(
child: Text('Sign In with Google'),
onPressed: () => _signIn(),
);
}, },
), );
}
void _showError() {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Unable to sign in.'),
), ),
); );
} }

@ -28,6 +28,7 @@ class AdaptiveScaffoldDestination {
/// defined in the [destinations] parameter. /// defined in the [destinations] parameter.
class AdaptiveScaffold extends StatefulWidget { class AdaptiveScaffold extends StatefulWidget {
final Widget title; final Widget title;
final List<Widget> actions;
final Widget body; final Widget body;
final int currentIndex; final int currentIndex;
final List<AdaptiveScaffoldDestination> destinations; final List<AdaptiveScaffoldDestination> destinations;
@ -37,6 +38,7 @@ class AdaptiveScaffold extends StatefulWidget {
AdaptiveScaffold({ AdaptiveScaffold({
this.title, this.title,
this.body, this.body,
this.actions = const [],
@required this.currentIndex, @required this.currentIndex,
@required this.destinations, @required this.destinations,
this.onNavigationIndexChange, this.onNavigationIndexChange,
@ -80,7 +82,9 @@ class _AdaptiveScaffoldState extends State<AdaptiveScaffold> {
), ),
Expanded( Expanded(
child: Scaffold( child: Scaffold(
appBar: AppBar(), appBar: AppBar(
actions: widget.actions,
),
body: widget.body, body: widget.body,
floatingActionButton: widget.floatingActionButton, floatingActionButton: widget.floatingActionButton,
), ),
@ -94,6 +98,7 @@ class _AdaptiveScaffoldState extends State<AdaptiveScaffold> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: widget.title, title: widget.title,
actions: widget.actions,
), ),
body: Row( body: Row(
children: [ children: [
@ -126,7 +131,10 @@ class _AdaptiveScaffoldState extends State<AdaptiveScaffold> {
// Show a bottom app bar // Show a bottom app bar
return Scaffold( return Scaffold(
body: widget.body, body: widget.body,
appBar: AppBar(title: widget.title), appBar: AppBar(
title: widget.title,
actions: widget.actions,
),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
items: [ items: [
...widget.destinations.map( ...widget.destinations.map(

Loading…
Cancel
Save