Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 2 additions & 27 deletions app_flutter/lib/build_dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'service/google_authentication.dart';
import 'sign_in_button.dart';
import 'state/flutter_build.dart';
import 'status_grid.dart';

Expand Down Expand Up @@ -54,7 +54,7 @@ class BuildDashboard extends StatelessWidget {
? theme.primaryColor
: theme.errorColor,
actions: <Widget>[
UserAvatar(buildState: buildState),
SignInButton(authService: buildState.authService),
],
),
body: Column(
Expand All @@ -66,28 +66,3 @@ class BuildDashboard extends StatelessWidget {
);
}
}

/// Widget for displaying sign in information for the current user.
///
/// If logged in, it will display the user's avatar. Otherwise, it will show
/// a button for sign in.
class UserAvatar extends StatelessWidget {
const UserAvatar({@required this.buildState, Key key}) : super(key: key);

final FlutterBuildState buildState;

@override
Widget build(BuildContext context) {
final GoogleSignInService authService = buildState.authService;

if (authService.isAuthenticated) {
/// Size needs to be specified
return Image.network(authService.avatarUrl + '=s100');
}

return FlatButton(
child: const Text('Sign in'),
onPressed: () => buildState.signIn(),
);
}
}
6 changes: 3 additions & 3 deletions app_flutter/lib/service/appengine_cocoon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ class AppEngineCocoonService implements CocoonService {
}

@override
Future<bool> rerunTask(Task task, String accessToken) async {
assert(accessToken != null);
Future<bool> rerunTask(Task task, String idToken) async {
assert(idToken != null);
final String postResetTaskUrl = _apiEndpoint('/api/reset-devicelab-task');

/// This endpoint only returns a status code.
final http.Response response = await _client.post(postResetTaskUrl,
headers: <String, String>{
'X-Flutter-AccessToken': accessToken,
'X-Flutter-IdToken': idToken,
},
body: jsonEncode(<String, String>{
'Key': task.key.child.name,
Expand Down
48 changes: 28 additions & 20 deletions app_flutter/lib/service/google_authentication.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:google_sign_in_all/google_sign_in_all.dart';
import 'package:flutter/rendering.dart';
import 'package:google_sign_in/google_sign_in.dart';

/// Service class for interacting with Google Sign In authentication for Cocoon backend.
class GoogleSignInService {
/// Creates a new [GoogleSignIn].
GoogleSignInService({GoogleSignIn googleSignIn})
GoogleSignInService({GoogleSignIn googleSignIn, this.notifyListeners})
: _googleSignIn = googleSignIn ??
setupGoogleSignIn(
GoogleSignIn(
scopes: _googleScopes,
webClientId:
'308150028417-vlj9mqlm3gk1d03fb0efif1fu5nagdtt.apps.googleusercontent.com',
);
) {
_googleSignIn.onCurrentUserChanged
.listen((GoogleSignInAccount accountValue) {
user = accountValue;
notifyListeners();
});
_googleSignIn.signInSilently();
}

/// A callback for notifying listeners there has been an update.
final VoidCallback notifyListeners;

/// A list of Google API OAuth Scopes this project needs access to.
///
Expand All @@ -24,30 +33,29 @@ class GoogleSignInService {
static const List<String> _googleScopes = <String>[
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'openid',
];

// TODO(chillers): Switch to official Flutter plugin when it supports web.
final GoogleSignIn _googleSignIn;

AuthCredentials _credentials;

GoogleAccount _user;

/// Whether or not the application has been signed in to.
bool get isAuthenticated => _credentials?.accessToken != null;
Future<bool> get isAuthenticated => _googleSignIn.isSignedIn();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't want to change the getter signature you may be able to do:

bool get isAuthenticated => await _googleSignIn.isSignedIn();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(You might need to change it to an async fn, so maybe you still need to change the signature after all, hmmm)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The await expression can only be used in an async function.
Try marking the function body with either 'async' or 'async*'


/// The profile photo url of the current user signed in.
String get avatarUrl => _user?.photoUrl;

/// The email of the current user signed in.
String get email => _user?.email;
/// The Google Account for the signed in user, null if no user is signed in.
///
/// Read only object with only access to clear client auth tokens.
GoogleSignInAccount user;

/// Authentication token to be sent to Cocoon Backend to verify API calls.
String get accessToken => _credentials?.accessToken;
Future<String> get idToken => user?.authentication
?.then((GoogleSignInAuthentication key) => key.idToken);

/// Initiate the Google Sign In process.
Future<void> signIn() async {
_credentials = await _googleSignIn.signIn();
_user = await _googleSignIn.getCurrentUser();
user = await _googleSignIn.signIn();
}

Future<void> signOut() async {
await _googleSignIn.signOut();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action required, just want to mention that it's amusing to see "SignIn.signOut" :)

}
}
53 changes: 53 additions & 0 deletions app_flutter/lib/sign_in_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2019 The Chromium Authors. 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/material.dart';
import 'package:flutter/widgets.dart';

import 'service/google_authentication.dart';

/// Widget for displaying sign in information for the current user.
///
/// If logged in, it will display the user's avatar. Clicking it opens a dropdown for logging out.
/// Otherwise, a sign in button will show.
class SignInButton extends StatelessWidget {
const SignInButton({@required this.authService, Key key}) : super(key: key);

final GoogleSignInService authService;

@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: authService.isAuthenticated,
builder: (_, AsyncSnapshot<bool> isAuthenticated) {
/// On sign out, there's a second where the user is null before isAuthenticated catches up.
if (isAuthenticated.data == true && authService.user != null) {
return PopupMenuButton<String>(
// TODO(chillers): Switch to use avatar widget provided by google_sign_in plugin
child: Image.network(authService.user?.photoUrl),
offset: const Offset(0, 50),
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'logout',
child: Text('Log out'),
),
],
onSelected: (String value) {
if (value == 'logout') {
authService.signOut();
}
},
);
}
return FlatButton(
child: const Text(
'Sign in',
style: TextStyle(color: Colors.white),
),
onPressed: () => authService.signIn(),
);
},
);
}
}
22 changes: 11 additions & 11 deletions app_flutter/lib/state/flutter_build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ class FlutterBuildState extends ChangeNotifier {
///
/// If [CocoonService] is not specified, a new [CocoonService] instance is created.
FlutterBuildState({
CocoonService cocoonService,
GoogleSignInService authService,
}) : authService = authService ?? GoogleSignInService(),
_cocoonService = cocoonService ?? CocoonService();
CocoonService cocoonServiceValue,
GoogleSignInService authServiceValue,
}) : _cocoonService = cocoonServiceValue ?? CocoonService() {
authService = authServiceValue ??
GoogleSignInService(notifyListeners: notifyListeners);
}

/// Cocoon backend service that retrieves the data needed for this state.
final CocoonService _cocoonService;

/// Authentication service for managing Google Sign In.
final GoogleSignInService authService;
GoogleSignInService authService;

/// How often to query the Cocoon backend for the current build state.
@visibleForTesting
Expand Down Expand Up @@ -88,13 +90,11 @@ class FlutterBuildState extends ChangeNotifier {
notifyListeners();
}

Future<void> signIn() async {
await authService.signIn();
notifyListeners();
}
Future<void> signIn() => authService.signIn();
Future<void> signOut() => authService.signOut();

Future<bool> rerunTask(Task task) {
return _cocoonService.rerunTask(task, authService.accessToken);
Future<bool> rerunTask(Task task) async {
return _cocoonService.rerunTask(task, await authService.idToken);
}

@override
Expand Down
2 changes: 1 addition & 1 deletion app_flutter/lib/task_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ class TaskOverlayContents extends StatelessWidget {
// Only send access token for devicelab tasks since they require authentication
final Map<String, String> headers = isDevicelab(task)
? <String, String>{
'X-Flutter-AccessToken': buildState.authService.accessToken,
'X-Flutter-IdToken': await buildState.authService.idToken,
}
: null;
launch(logUrl(task), headers: headers);
Expand Down
11 changes: 9 additions & 2 deletions app_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ description: A new Flutter project.
version: 1.0.0+1

environment:
sdk: ">=2.3.0 <3.0.0"
sdk: ">=2.6.0 <3.0.0"

dependencies:
flutter:
Expand All @@ -31,7 +31,14 @@ dependencies:
cocoon_service:
path: ../app_dart
flutter_progress_button: ^1.0.0
google_sign_in_all: ^0.0.3
google_sign_in: ^4.0.14
google_sign_in_web:
git:
# TODO(chillers): https://github.com/flutter/plugins/pull/2280 and the Google Sign In plugin
# publishes the update switch to the official version.
url: git://github.com/ditman/plugins.git
ref: federated_google_sign_in_web
path: packages/google_sign_in/google_sign_in_web
Comment on lines +39 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had no idea you could point to a repo+branch when using the git source. Nice!

provider: ^3.0.0
url_launcher: 5.2.4
url_launcher_web:
Expand Down
70 changes: 10 additions & 60 deletions app_flutter/test/build_dashboard_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,22 @@

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart' as test;
import 'package:provider/provider.dart';

import 'package:app_flutter/build_dashboard.dart';
import 'package:app_flutter/service/google_authentication.dart';
import 'package:app_flutter/service/fake_cocoon.dart';
import 'package:app_flutter/sign_in_button.dart';
import 'package:app_flutter/state/flutter_build.dart';

void main() {
group('UserAvatar', () {
GoogleSignInService authService;
testWidgets('shows sign in button', (WidgetTester tester) async {
final FlutterBuildState buildState = FlutterBuildState();

setUp(() {
authService = MockGoogleSignInService();
});
await tester.pumpWidget(MaterialApp(
home: ChangeNotifierProvider<FlutterBuildState>(
builder: (_) => buildState,
child: BuildDashboard(),
)));

testWidgets('shows sign in button when not signed in',
(WidgetTester tester) async {
when(authService.isAuthenticated).thenReturn(false);
final FlutterBuildState buildState = FlutterBuildState(
authService: authService, cocoonService: FakeCocoonService());
await tester.pumpWidget(MaterialApp(
home: UserAvatar(
buildState: buildState,
),
));

expect(find.text('Sign in'), findsOneWidget);
});

testWidgets('sign in button activates google sign in when pressed',
(WidgetTester tester) async {
when(authService.isAuthenticated).thenReturn(false);
final FlutterBuildState buildState = FlutterBuildState(
authService: authService, cocoonService: FakeCocoonService());
await tester.pumpWidget(MaterialApp(
home: UserAvatar(
buildState: buildState,
),
));

verifyNever(authService.signIn());

await tester.tap(find.byType(UserAvatar));

verify(authService.signIn()).called(1);
});

testWidgets('shows user avatar when signed in',
(WidgetTester tester) async {
when(authService.isAuthenticated).thenReturn(true);
when(authService.avatarUrl).thenReturn('https://flutter.dev');
final FlutterBuildState buildState = FlutterBuildState(
authService: authService, cocoonService: FakeCocoonService());
await tester.pumpWidget(MaterialApp(
home: UserAvatar(
buildState: buildState,
),
));

expect(tester.takeException(),
const test.TypeMatcher<NetworkImageLoadException>());
expect(find.byType(Image), findsOneWidget);
});
expect(find.byType(SignInButton), findsOneWidget);
});
}

/// Mock [GoogleSignInService] for testing interactions.
class MockGoogleSignInService extends Mock implements GoogleSignInService {}
Loading