From 8a9bcfa11379563ea566167e7754165ffe29c3c1 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Mon, 1 Jun 2020 14:39:06 -0700 Subject: [PATCH] [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 --- experimental/web_dashboard/lib/src/app.dart | 77 ++++++++++------ .../web_dashboard/lib/src/auth/auth.dart | 3 + .../web_dashboard/lib/src/auth/firebase.dart | 20 ++++- .../web_dashboard/lib/src/auth/mock.dart | 9 ++ .../web_dashboard/lib/src/pages/home.dart | 48 +++++++++- .../web_dashboard/lib/src/pages/sign_in.dart | 90 +++++++++++++++---- .../third_party/adaptive_scaffold.dart | 12 ++- 7 files changed, 211 insertions(+), 48 deletions(-) diff --git a/experimental/web_dashboard/lib/src/app.dart b/experimental/web_dashboard/lib/src/app.dart index 1daacfe8e..f40cc5b8c 100644 --- a/experimental/web_dashboard/lib/src/app.dart +++ b/experimental/web_dashboard/lib/src/app.dart @@ -23,7 +23,9 @@ class AppState { 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); /// An app that displays a personalized dashboard. @@ -63,41 +65,62 @@ class _DashboardAppState extends State { return Provider.value( value: _appState, child: MaterialApp( - home: Builder( - builder: (context) => SignInPage( - auth: _appState.auth, - onSuccess: (user) => _handleSignIn(user, context, _appState), - ), + home: SignInSwitcher( + appState: _appState, + apiBuilder: widget.apiBuilder, ), ), ); } +} + +/// 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. - void _handleSignIn(User user, BuildContext context, AppState appState) { - appState.api = widget.apiBuilder(user); +class _SignInSwitcherState extends State { + bool _isSignedIn = false; - _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 _showPage(Widget page, BuildContext context) { - var route = _fadeRoute(page); - Navigator.of(context).pushReplacement(route); + void _handleSignIn(User user) { + widget.appState.api = widget.apiBuilder(user); + + setState(() { + _isSignedIn = true; + }); } - /// Creates a [Route] that shows [newPage] using a fade transition. - Route _fadeRoute(Widget newPage) { - return PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) { - return newPage; - }, - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition( - opacity: animation.drive(CurveTween(curve: Curves.ease)), - child: child, - ); - }, - ); + Future _handleSignOut() async { + await widget.appState.auth.signOut(); + setState(() { + _isSignedIn = false; + }); } } diff --git a/experimental/web_dashboard/lib/src/auth/auth.dart b/experimental/web_dashboard/lib/src/auth/auth.dart index dd02bae28..24cb700b6 100644 --- a/experimental/web_dashboard/lib/src/auth/auth.dart +++ b/experimental/web_dashboard/lib/src/auth/auth.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. abstract class Auth { + Future get isSignedIn; Future signIn(); Future signOut(); } @@ -10,3 +11,5 @@ abstract class Auth { abstract class User { String get uid; } + +class SignInException implements Exception {} diff --git a/experimental/web_dashboard/lib/src/auth/firebase.dart b/experimental/web_dashboard/lib/src/auth/firebase.dart index f11a02c0f..5c8b7807a 100644 --- a/experimental/web_dashboard/lib/src/auth/firebase.dart +++ b/experimental/web_dashboard/lib/src/auth/firebase.dart @@ -2,6 +2,7 @@ // 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. +import 'package:flutter/services.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:firebase_auth/firebase_auth.dart' hide FirebaseUser; @@ -11,9 +12,20 @@ class FirebaseAuthService implements Auth { final GoogleSignIn _googleSignIn = GoogleSignIn(); final FirebaseAuth _auth = FirebaseAuth.instance; + Future get isSignedIn => _googleSignIn.isSignedIn(); + Future signIn() async { + try { + return await _signIn(); + } on PlatformException catch (e) { + print('Unable to sign in: $e'); + throw SignInException(); + } + } + + Future _signIn() async { GoogleSignInAccount googleUser; - if (await _googleSignIn.isSignedIn()) { + if (await isSignedIn) { googleUser = await _googleSignIn.signInSilently(); } else { googleUser = await _googleSignIn.signIn(); @@ -30,7 +42,10 @@ class FirebaseAuthService implements Auth { } Future signOut() async { - await _auth.signOut(); + await Future.wait([ + _auth.signOut(), + _googleSignIn.signOut(), + ]); } } @@ -39,3 +54,4 @@ class _FirebaseUser implements User { _FirebaseUser(this.uid); } + diff --git a/experimental/web_dashboard/lib/src/auth/mock.dart b/experimental/web_dashboard/lib/src/auth/mock.dart index 45766a67d..a1bdcd16c 100644 --- a/experimental/web_dashboard/lib/src/auth/mock.dart +++ b/experimental/web_dashboard/lib/src/auth/mock.dart @@ -2,11 +2,20 @@ // 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. +import 'dart:math'; + import 'auth.dart'; class MockAuthService implements Auth { + Future get isSignedIn async => false; + @override Future signIn() async { + // Sign in will randomly fail 25% of the time. + var random = Random(); + if (random.nextInt(4) == 0) { + throw SignInException(); + } return MockUser(); } diff --git a/experimental/web_dashboard/lib/src/pages/home.dart b/experimental/web_dashboard/lib/src/pages/home.dart index 868fb4844..0c22935b0 100644 --- a/experimental/web_dashboard/lib/src/pages/home.dart +++ b/experimental/web_dashboard/lib/src/pages/home.dart @@ -10,6 +10,12 @@ import 'dashboard.dart'; import 'entries.dart'; class HomePage extends StatefulWidget { + final VoidCallback onSignOut; + + HomePage({ + @required this.onSignOut, + }); + @override _HomePageState createState() => _HomePageState(); } @@ -20,6 +26,17 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { 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, destinations: [ AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home), @@ -67,7 +84,36 @@ class _HomePageState extends State { } } - Widget _pageAtIndex(int index) { + Future _handleSignOut() async { + var shouldSignOut = await showDialog( + 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) { return DashboardPage(); } diff --git a/experimental/web_dashboard/lib/src/pages/sign_in.dart b/experimental/web_dashboard/lib/src/pages/sign_in.dart index 04d79e975..f5f8f5ae6 100644 --- a/experimental/web_dashboard/lib/src/pages/sign_in.dart +++ b/experimental/web_dashboard/lib/src/pages/sign_in.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import '../auth/auth.dart'; -class SignInPage extends StatefulWidget { +class SignInPage extends StatelessWidget { final Auth auth; final ValueChanged onSuccess; @@ -16,30 +16,88 @@ class SignInPage extends StatefulWidget { }); @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 onSuccess; + + SignInButton({ + @required this.auth, + @required this.onSuccess, + }); + + @override + _SignInButtonState createState() => _SignInButtonState(); } -class _SignInPageState extends State { +class _SignInButtonState extends State { + Future _checkSignInFuture; + @override void initState() { super.initState(); + _checkSignInFuture = _checkIfSignedIn(); + } + + // Check if the user is signed in. If the user is already signed in (for + // example, if they signed in and refreshed the page), invoke the `onSuccess` + // callback right away. + Future _checkIfSignedIn() async { + var alreadySignedIn = await widget.auth.isSignedIn; + if (alreadySignedIn) { + var user = await widget.auth.signIn(); + widget.onSuccess(user); + } + return alreadySignedIn; + } + + Future _signIn() async { + try { + var user = await widget.auth.signIn(); + widget.onSuccess(user); + } on SignInException { + _showError(); + } } @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: RaisedButton( - child: Text('Sign In'), - onPressed: () async { - var user = await widget.auth.signIn(); - if (user != null) { - widget.onSuccess(user); - } else { - throw ('Unable to sign in'); - } - }, - ), + return FutureBuilder( + 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.'), ), ); } diff --git a/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart b/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart index c5308ee41..4b20b0e4b 100644 --- a/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart +++ b/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart @@ -28,6 +28,7 @@ class AdaptiveScaffoldDestination { /// defined in the [destinations] parameter. class AdaptiveScaffold extends StatefulWidget { final Widget title; + final List actions; final Widget body; final int currentIndex; final List destinations; @@ -37,6 +38,7 @@ class AdaptiveScaffold extends StatefulWidget { AdaptiveScaffold({ this.title, this.body, + this.actions = const [], @required this.currentIndex, @required this.destinations, this.onNavigationIndexChange, @@ -80,7 +82,9 @@ class _AdaptiveScaffoldState extends State { ), Expanded( child: Scaffold( - appBar: AppBar(), + appBar: AppBar( + actions: widget.actions, + ), body: widget.body, floatingActionButton: widget.floatingActionButton, ), @@ -94,6 +98,7 @@ class _AdaptiveScaffoldState extends State { return Scaffold( appBar: AppBar( title: widget.title, + actions: widget.actions, ), body: Row( children: [ @@ -126,7 +131,10 @@ class _AdaptiveScaffoldState extends State { // Show a bottom app bar return Scaffold( body: widget.body, - appBar: AppBar(title: widget.title), + appBar: AppBar( + title: widget.title, + actions: widget.actions, + ), bottomNavigationBar: BottomNavigationBar( items: [ ...widget.destinations.map(