Skip to content

Commit 39678d1

Browse files
authored
[google_sign_in] Add server auth code retrieval to gis_client (#5358)
Adds the ability for a Flutter web app to request a server auth code via gis through a web-only method. Reference docs: * https://developers.google.com/identity/oauth2/web/guides/use-code-model * https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.initCodeClient Also: adds a `web_only.dart` library that allows programmers to call web-only methods conveniently, without having to dive into the `Platform.instance` (which has a ton of methods that we don't want users to call for sure!), like this: ```dart import 'package:google_sign_in_web/web_only.dart' as web; /// Renders a web-only Sign-In button. Widget buildSignInButton({HandleSignInFn? _}) { return web.renderButton(); } ``` Instead of: https://github.com/flutter/packages/blob/a6821898bd5a968f8ddafa8ae8e9a8c889caa00a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart#L12-L15 ### Issues * Fixes flutter/flutter#62474 ### Tests * Added tests to ensure plugin calls hit the GIS API when appropriate. * [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. * [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. * [x] I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use dart format.) * [x] I signed the [CLA]. * [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences] * [x] I listed at least one issue that this PR fixes in the description above. * [x] I updated pubspec.yaml with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. * [x] I updated CHANGELOG.md to add a description of the change, [following repository CHANGELOG style]. * [x] I updated/added relevant documentation (doc comments with ///). * [x] I added new tests to check the change I am making, or this PR is [test-exempt]. * [x] All existing and new tests are passing.
1 parent 9cdc001 commit 39678d1

File tree

10 files changed

+419
-3
lines changed

10 files changed

+419
-3
lines changed

packages/google_sign_in/google_sign_in_web/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.12.2
2+
3+
* Adds server auth code retrieval to google_sign_in_web.
4+
* Adds `web_only` library to access web-only methods more easily.
5+
16
## 0.12.1
27

38
* Enables FedCM on browsers that support this authentication mechanism.

packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,24 @@ void main() {
250250
expect(arguments.elementAt(1), someAccessToken);
251251
});
252252
});
253+
254+
group('requestServerAuthCode', () {
255+
const String someAuthCode = '50m3_4u7h_c0d3';
256+
257+
setUp(() {
258+
plugin.initWithParams(options);
259+
});
260+
261+
testWidgets('passes-through call to gis client', (_) async {
262+
mockito
263+
.when(mockGis.requestServerAuthCode())
264+
.thenAnswer((_) => Future<String>.value(someAuthCode));
265+
266+
final String? serverAuthCode = await plugin.requestServerAuthCode();
267+
268+
expect(serverAuthCode, someAuthCode);
269+
});
270+
});
253271
});
254272

255273
group('userDataEvents', () {

packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
// Mocks generated by Mockito 5.4.0 from annotations
1+
// Mocks generated by Mockito 5.4.1 from annotations
22
// in google_sign_in_web_integration_tests/integration_test/google_sign_in_web_test.dart.
33
// Do not manually edit this file.
44

5+
// @dart=2.19
6+
57
// ignore_for_file: no_leading_underscores_for_library_prefixes
68
import 'dart:async' as _i4;
79

@@ -47,6 +49,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
4749
returnValueForMissingStub:
4850
_i4.Future<_i2.GoogleSignInUserData?>.value(),
4951
) as _i4.Future<_i2.GoogleSignInUserData?>);
52+
5053
@override
5154
_i4.Future<void> renderButton(
5255
Object? parent,
@@ -63,6 +66,17 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
6366
returnValue: _i4.Future<void>.value(),
6467
returnValueForMissingStub: _i4.Future<void>.value(),
6568
) as _i4.Future<void>);
69+
70+
@override
71+
_i4.Future<String?> requestServerAuthCode() => (super.noSuchMethod(
72+
Invocation.method(
73+
#requestServerAuthCode,
74+
[],
75+
),
76+
returnValue: _i4.Future<String?>.value(),
77+
returnValueForMissingStub: _i4.Future<String?>.value(),
78+
) as _i4.Future<String?>);
79+
6680
@override
6781
_i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod(
6882
Invocation.method(
@@ -73,6 +87,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
7387
returnValueForMissingStub:
7488
_i4.Future<_i2.GoogleSignInUserData?>.value(),
7589
) as _i4.Future<_i2.GoogleSignInUserData?>);
90+
7691
@override
7792
_i2.GoogleSignInTokenData getTokens() => (super.noSuchMethod(
7893
Invocation.method(
@@ -94,6 +109,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
94109
),
95110
),
96111
) as _i2.GoogleSignInTokenData);
112+
97113
@override
98114
_i4.Future<void> signOut() => (super.noSuchMethod(
99115
Invocation.method(
@@ -103,6 +119,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
103119
returnValue: _i4.Future<void>.value(),
104120
returnValueForMissingStub: _i4.Future<void>.value(),
105121
) as _i4.Future<void>);
122+
106123
@override
107124
_i4.Future<void> disconnect() => (super.noSuchMethod(
108125
Invocation.method(
@@ -112,6 +129,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
112129
returnValue: _i4.Future<void>.value(),
113130
returnValueForMissingStub: _i4.Future<void>.value(),
114131
) as _i4.Future<void>);
132+
115133
@override
116134
_i4.Future<bool> isSignedIn() => (super.noSuchMethod(
117135
Invocation.method(
@@ -121,6 +139,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
121139
returnValue: _i4.Future<bool>.value(false),
122140
returnValueForMissingStub: _i4.Future<bool>.value(false),
123141
) as _i4.Future<bool>);
142+
124143
@override
125144
_i4.Future<void> clearAuthCache() => (super.noSuchMethod(
126145
Invocation.method(
@@ -130,6 +149,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
130149
returnValue: _i4.Future<void>.value(),
131150
returnValueForMissingStub: _i4.Future<void>.value(),
132151
) as _i4.Future<void>);
152+
133153
@override
134154
_i4.Future<bool> requestScopes(List<String>? scopes) => (super.noSuchMethod(
135155
Invocation.method(
@@ -139,6 +159,7 @@ class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
139159
returnValue: _i4.Future<bool>.value(false),
140160
returnValueForMissingStub: _i4.Future<bool>.value(false),
141161
) as _i4.Future<bool>);
162+
142163
@override
143164
_i4.Future<bool> canAccessScopes(
144165
List<String>? scopes,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
7+
import 'package:google_sign_in_web/google_sign_in_web.dart'
8+
show GoogleSignInPlugin;
9+
import 'package:google_sign_in_web/src/gis_client.dart';
10+
import 'package:google_sign_in_web/web_only.dart' as web;
11+
import 'package:integration_test/integration_test.dart';
12+
import 'package:mockito/annotations.dart';
13+
import 'package:mockito/mockito.dart' as mockito;
14+
15+
import 'web_only_test.mocks.dart';
16+
17+
// Mock GisSdkClient so we can simulate any response from the JS side.
18+
@GenerateMocks(<Type>[], customMocks: <MockSpec<dynamic>>[
19+
MockSpec<GisSdkClient>(onMissingStub: OnMissingStub.returnDefault),
20+
])
21+
void main() {
22+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
23+
24+
group('non-web plugin instance', () {
25+
setUp(() {
26+
GoogleSignInPlatform.instance = NonWebImplementation();
27+
});
28+
29+
testWidgets('renderButton throws', (WidgetTester _) async {
30+
expect(() {
31+
web.renderButton();
32+
}, throwsAssertionError);
33+
});
34+
35+
testWidgets('requestServerAuthCode throws', (WidgetTester _) async {
36+
expect(() async {
37+
await web.requestServerAuthCode();
38+
}, throwsAssertionError);
39+
});
40+
});
41+
42+
group('web plugin instance', () {
43+
const String someAuthCode = '50m3_4u7h_c0d3';
44+
late MockGisSdkClient mockGis;
45+
46+
setUp(() {
47+
mockGis = MockGisSdkClient();
48+
GoogleSignInPlatform.instance = GoogleSignInPlugin(
49+
debugOverrideLoader: true,
50+
debugOverrideGisSdkClient: mockGis,
51+
)..initWithParams(
52+
const SignInInitParameters(
53+
clientId: 'does-not-matter',
54+
),
55+
);
56+
});
57+
58+
testWidgets('call reaches GIS API', (WidgetTester _) async {
59+
mockito
60+
.when(mockGis.requestServerAuthCode())
61+
.thenAnswer((_) => Future<String>.value(someAuthCode));
62+
63+
final String? serverAuthCode = await web.requestServerAuthCode();
64+
65+
expect(serverAuthCode, someAuthCode);
66+
});
67+
});
68+
}
69+
70+
/// Fake non-web implementation used to verify that the web_only methods
71+
/// throw when the wrong type of instance is configured.
72+
class NonWebImplementation extends GoogleSignInPlatform {}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Mocks generated by Mockito 5.4.1 from annotations
2+
// in google_sign_in_web_integration_tests/integration_test/web_only_test.dart.
3+
// Do not manually edit this file.
4+
5+
// @dart=2.19
6+
7+
// ignore_for_file: no_leading_underscores_for_library_prefixes
8+
import 'dart:async' as _i4;
9+
10+
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'
11+
as _i2;
12+
import 'package:google_sign_in_web/src/button_configuration.dart' as _i5;
13+
import 'package:google_sign_in_web/src/gis_client.dart' as _i3;
14+
import 'package:mockito/mockito.dart' as _i1;
15+
16+
// ignore_for_file: type=lint
17+
// ignore_for_file: avoid_redundant_argument_values
18+
// ignore_for_file: avoid_setters_without_getters
19+
// ignore_for_file: comment_references
20+
// ignore_for_file: implementation_imports
21+
// ignore_for_file: invalid_use_of_visible_for_testing_member
22+
// ignore_for_file: prefer_const_constructors
23+
// ignore_for_file: unnecessary_parenthesis
24+
// ignore_for_file: camel_case_types
25+
// ignore_for_file: subtype_of_sealed_class
26+
27+
class _FakeGoogleSignInTokenData_0 extends _i1.SmartFake
28+
implements _i2.GoogleSignInTokenData {
29+
_FakeGoogleSignInTokenData_0(
30+
Object parent,
31+
Invocation parentInvocation,
32+
) : super(
33+
parent,
34+
parentInvocation,
35+
);
36+
}
37+
38+
/// A class which mocks [GisSdkClient].
39+
///
40+
/// See the documentation for Mockito's code generation for more information.
41+
class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
42+
@override
43+
_i4.Future<_i2.GoogleSignInUserData?> signInSilently() => (super.noSuchMethod(
44+
Invocation.method(
45+
#signInSilently,
46+
[],
47+
),
48+
returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(),
49+
returnValueForMissingStub:
50+
_i4.Future<_i2.GoogleSignInUserData?>.value(),
51+
) as _i4.Future<_i2.GoogleSignInUserData?>);
52+
53+
@override
54+
_i4.Future<void> renderButton(
55+
Object? parent,
56+
_i5.GSIButtonConfiguration? options,
57+
) =>
58+
(super.noSuchMethod(
59+
Invocation.method(
60+
#renderButton,
61+
[
62+
parent,
63+
options,
64+
],
65+
),
66+
returnValue: _i4.Future<void>.value(),
67+
returnValueForMissingStub: _i4.Future<void>.value(),
68+
) as _i4.Future<void>);
69+
70+
@override
71+
_i4.Future<String?> requestServerAuthCode() => (super.noSuchMethod(
72+
Invocation.method(
73+
#requestServerAuthCode,
74+
[],
75+
),
76+
returnValue: _i4.Future<String?>.value(),
77+
returnValueForMissingStub: _i4.Future<String?>.value(),
78+
) as _i4.Future<String?>);
79+
80+
@override
81+
_i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod(
82+
Invocation.method(
83+
#signIn,
84+
[],
85+
),
86+
returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(),
87+
returnValueForMissingStub:
88+
_i4.Future<_i2.GoogleSignInUserData?>.value(),
89+
) as _i4.Future<_i2.GoogleSignInUserData?>);
90+
91+
@override
92+
_i2.GoogleSignInTokenData getTokens() => (super.noSuchMethod(
93+
Invocation.method(
94+
#getTokens,
95+
[],
96+
),
97+
returnValue: _FakeGoogleSignInTokenData_0(
98+
this,
99+
Invocation.method(
100+
#getTokens,
101+
[],
102+
),
103+
),
104+
returnValueForMissingStub: _FakeGoogleSignInTokenData_0(
105+
this,
106+
Invocation.method(
107+
#getTokens,
108+
[],
109+
),
110+
),
111+
) as _i2.GoogleSignInTokenData);
112+
113+
@override
114+
_i4.Future<void> signOut() => (super.noSuchMethod(
115+
Invocation.method(
116+
#signOut,
117+
[],
118+
),
119+
returnValue: _i4.Future<void>.value(),
120+
returnValueForMissingStub: _i4.Future<void>.value(),
121+
) as _i4.Future<void>);
122+
123+
@override
124+
_i4.Future<void> disconnect() => (super.noSuchMethod(
125+
Invocation.method(
126+
#disconnect,
127+
[],
128+
),
129+
returnValue: _i4.Future<void>.value(),
130+
returnValueForMissingStub: _i4.Future<void>.value(),
131+
) as _i4.Future<void>);
132+
133+
@override
134+
_i4.Future<bool> isSignedIn() => (super.noSuchMethod(
135+
Invocation.method(
136+
#isSignedIn,
137+
[],
138+
),
139+
returnValue: _i4.Future<bool>.value(false),
140+
returnValueForMissingStub: _i4.Future<bool>.value(false),
141+
) as _i4.Future<bool>);
142+
143+
@override
144+
_i4.Future<void> clearAuthCache() => (super.noSuchMethod(
145+
Invocation.method(
146+
#clearAuthCache,
147+
[],
148+
),
149+
returnValue: _i4.Future<void>.value(),
150+
returnValueForMissingStub: _i4.Future<void>.value(),
151+
) as _i4.Future<void>);
152+
153+
@override
154+
_i4.Future<bool> requestScopes(List<String>? scopes) => (super.noSuchMethod(
155+
Invocation.method(
156+
#requestScopes,
157+
[scopes],
158+
),
159+
returnValue: _i4.Future<bool>.value(false),
160+
returnValueForMissingStub: _i4.Future<bool>.value(false),
161+
) as _i4.Future<bool>);
162+
163+
@override
164+
_i4.Future<bool> canAccessScopes(
165+
List<String>? scopes,
166+
String? accessToken,
167+
) =>
168+
(super.noSuchMethod(
169+
Invocation.method(
170+
#canAccessScopes,
171+
[
172+
scopes,
173+
accessToken,
174+
],
175+
),
176+
returnValue: _i4.Future<bool>.value(false),
177+
returnValueForMissingStub: _i4.Future<bool>.value(false),
178+
) as _i4.Future<bool>);
179+
}

packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,11 @@ class GoogleSignInPlugin extends GoogleSignInPlatform {
300300
@override
301301
Stream<GoogleSignInUserData?>? get userDataEvents =>
302302
_userDataController.stream;
303+
304+
/// Requests server auth code from GIS Client per:
305+
/// https://developers.google.com/identity/oauth2/web/guides/use-code-model#initialize_a_code_client
306+
Future<String?> requestServerAuthCode() async {
307+
await initialized;
308+
return _gisClient.requestServerAuthCode();
309+
}
303310
}

0 commit comments

Comments
 (0)