mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
[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
This commit is contained in:
@@ -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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the DashboardApi on AppState and navigates to the home page.
|
/// Switches between showing the [SignInPage] or [HomePage], depending on
|
||||||
void _handleSignIn(User user, BuildContext context, AppState appState) {
|
/// whether or not the user is signed in.
|
||||||
appState.api = widget.apiBuilder(user);
|
class SignInSwitcher extends StatefulWidget {
|
||||||
|
final AppState appState;
|
||||||
|
final ApiBuilder apiBuilder;
|
||||||
|
|
||||||
_showPage(HomePage(), context);
|
SignInSwitcher({
|
||||||
}
|
this.appState,
|
||||||
|
this.apiBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
/// Navigates to the home page using a fade transition.
|
@override
|
||||||
void _showPage(Widget page, BuildContext context) {
|
_SignInSwitcherState createState() => _SignInSwitcherState();
|
||||||
var route = _fadeRoute(page);
|
}
|
||||||
Navigator.of(context).pushReplacement(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [Route] that shows [newPage] using a fade transition.
|
class _SignInSwitcherState extends State<SignInSwitcher> {
|
||||||
Route<FadeTransition> _fadeRoute(Widget newPage) {
|
bool _isSignedIn = false;
|
||||||
return PageRouteBuilder<FadeTransition>(
|
|
||||||
pageBuilder: (context, animation, secondaryAnimation) {
|
@override
|
||||||
return newPage;
|
Widget build(BuildContext context) {
|
||||||
},
|
return AnimatedSwitcher(
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
switchInCurve: Curves.easeOut,
|
||||||
return FadeTransition(
|
switchOutCurve: Curves.easeOut,
|
||||||
opacity: animation.drive(CurveTween(curve: Curves.ease)),
|
duration: Duration(milliseconds: 200),
|
||||||
child: child,
|
child: _isSignedIn
|
||||||
);
|
? HomePage(
|
||||||
},
|
onSignOut: _handleSignOut,
|
||||||
|
)
|
||||||
|
: SignInPage(
|
||||||
|
auth: widget.appState.auth,
|
||||||
|
onSuccess: _handleSignIn,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleSignIn(User user) {
|
||||||
|
widget.appState.api = widget.apiBuilder(user);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSignedIn = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _handleSignOut() async {
|
||||||
|
await widget.appState.auth.signOut();
|
||||||
|
setState(() {
|
||||||
|
_isSignedIn = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -15,31 +15,89 @@ class SignInPage extends StatefulWidget {
|
|||||||
@required this.onSuccess,
|
@required this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
_SignInPageState createState() => _SignInPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SignInPageState extends State<SignInPage> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: RaisedButton(
|
child: SignInButton(auth: auth, onSuccess: onSuccess),
|
||||||
child: Text('Sign In'),
|
),
|
||||||
onPressed: () async {
|
);
|
||||||
var user = await widget.auth.signIn();
|
}
|
||||||
if (user != null) {
|
}
|
||||||
widget.onSuccess(user);
|
|
||||||
} else {
|
class SignInButton extends StatefulWidget {
|
||||||
throw ('Unable to sign in');
|
final Auth auth;
|
||||||
}
|
final ValueChanged<User> onSuccess;
|
||||||
},
|
|
||||||
),
|
SignInButton({
|
||||||
|
@required this.auth,
|
||||||
|
@required this.onSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SignInButtonState createState() => _SignInButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SignInButtonState extends State<SignInButton> {
|
||||||
|
Future<bool> _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<bool> _checkIfSignedIn() async {
|
||||||
|
var alreadySignedIn = await widget.auth.isSignedIn;
|
||||||
|
if (alreadySignedIn) {
|
||||||
|
var user = await widget.auth.signIn();
|
||||||
|
widget.onSuccess(user);
|
||||||
|
}
|
||||||
|
return alreadySignedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _signIn() async {
|
||||||
|
try {
|
||||||
|
var user = await widget.auth.signIn();
|
||||||
|
widget.onSuccess(user);
|
||||||
|
} on SignInException {
|
||||||
|
_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(
|
||||||
|
|||||||
Reference in New Issue
Block a user