Skip to content

Commit 9e3846e

Browse files
authored
feat(dart): update the userInfo api response type (#36)
1 parent c25831d commit 9e3846e

File tree

6 files changed

+139
-34
lines changed

6 files changed

+139
-34
lines changed

example/lib/main.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ class _MyHomePageState extends State<MyHomePage> {
4343

4444
final redirectUri = 'io.logto://callback';
4545
final config = const LogtoConfig(
46-
appId: 'xgSxW0MDpVqW2GDvCnlNb', endpoint: 'https://logto.dev');
46+
appId: '<your-app-id>',
47+
endpoint: 'http://localhost:3001',
48+
scopes: ['email', 'phone']);
4749

4850
late LogtoClient logtoClient;
4951

@@ -105,6 +107,21 @@ class _MyHomePageState extends State<MyHomePage> {
105107
child: const Text('Sign Out'),
106108
);
107109

110+
Widget getUserInfoButton = TextButton(
111+
style: TextButton.styleFrom(
112+
foregroundColor: Colors.black,
113+
padding: const EdgeInsets.all(16.0),
114+
textStyle: const TextStyle(fontSize: 20),
115+
),
116+
onPressed: () async {
117+
var userInfo = await logtoClient.getUserInfo();
118+
setState(() {
119+
content = userInfo.toJson().toString();
120+
});
121+
},
122+
child: const Text('Get User Info'),
123+
);
124+
108125
return Scaffold(
109126
appBar: AppBar(
110127
title: Text(widget.title),
@@ -126,6 +143,11 @@ class _MyHomePageState extends State<MyHomePage> {
126143
? isAuthenticated == true
127144
? signOutButton
128145
: signInButton
146+
: const SizedBox.shrink(),
147+
isAuthenticated != null
148+
? isAuthenticated == true
149+
? getUserInfoButton
150+
: const SizedBox.shrink()
129151
: const SizedBox.shrink()
130152
],
131153
),

lib/logto_client.dart

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter_web_auth/flutter_web_auth.dart';
22
import 'package:http/http.dart' as http;
33
import 'package:jose/jose.dart';
4+
import 'package:logto_dart_sdk/src/interfaces/logto_user_info_response.dart';
45

56
import '/src/exceptions/logto_auth_exceptions.dart';
67
import '/src/interfaces/logto_interfaces.dart';
@@ -68,20 +69,23 @@ class LogtoClient {
6869
return _oidcConfig!;
6970
}
7071

71-
Future<AccessToken?> getAccessToken({String? resource}) async {
72+
Future<AccessToken?> getAccessToken(
73+
{String? resource, List<String>? scopes}) async {
7274
final accessToken = await _tokenStorage.getAccessToken(resource);
7375

7476
if (accessToken != null) {
7577
return accessToken;
7678
}
7779

78-
final token = await _getAccessTokenByRefreshToken(resource);
80+
final token =
81+
await _getAccessTokenByRefreshToken(resource: resource, scopes: scopes);
7982

8083
return token;
8184
}
8285

8386
// RBAC are not supported currently, no resource specific scopes are needed
84-
Future<AccessToken?> _getAccessTokenByRefreshToken(String? resource) async {
87+
Future<AccessToken?> _getAccessTokenByRefreshToken(
88+
{String? resource, List<String>? scopes}) async {
8589
final refreshToken = await _tokenStorage.refreshToken;
8690

8791
if (refreshToken == null) {
@@ -99,12 +103,15 @@ class LogtoClient {
99103
tokenEndPoint: oidcConfig.tokenEndpoint,
100104
clientId: config.appId,
101105
refreshToken: refreshToken,
102-
resource: resource);
106+
resource: resource,
107+
scopes: scopes);
103108

104-
final scopes = response.scope.split(' ');
109+
final responseScopes = response.scope.split(' ');
105110

106111
await _tokenStorage.setAccessToken(response.accessToken,
107-
expiresIn: response.expiresIn, resource: resource, scopes: scopes);
112+
expiresIn: response.expiresIn,
113+
resource: resource,
114+
scopes: responseScopes);
108115

109116
// renew refresh token
110117
await _tokenStorage.setRefreshToken(response.refreshToken);
@@ -116,7 +123,7 @@ class LogtoClient {
116123
await _tokenStorage.setIdToken(idToken);
117124
}
118125

119-
return await _tokenStorage.getAccessToken(resource, scopes);
126+
return await _tokenStorage.getAccessToken(resource, responseScopes);
120127
} finally {
121128
if (_httpClient == null) httpClient.close();
122129
}
@@ -248,4 +255,29 @@ class LogtoClient {
248255
}
249256
}
250257
}
258+
259+
Future<LogtoUserInfoResponse> getUserInfo() async {
260+
final httpClient = _httpClient ?? http.Client();
261+
262+
try {
263+
final oidcConfig = await _getOidcConfig(httpClient);
264+
265+
final accessToken = await _tokenStorage.getAccessToken();
266+
267+
if (accessToken == null) {
268+
throw LogtoAuthException(
269+
LogtoAuthExceptions.authenticationError, 'not authenticated');
270+
}
271+
272+
final userInfoResponse = await logto_core.fetchUserInfo(
273+
httpClient: httpClient,
274+
userInfoEndpoint: oidcConfig.userInfoEndpoint,
275+
accessToken: accessToken.token,
276+
);
277+
278+
return userInfoResponse;
279+
} finally {
280+
if (_httpClient == null) httpClient.close();
281+
}
282+
}
251283
}

lib/src/interfaces/logto_user_info_response.dart

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,41 @@ import 'package:json_annotation/json_annotation.dart';
22

33
part 'logto_user_info_response.g.dart';
44

5-
@JsonSerializable()
5+
@JsonSerializable(includeIfNull: false)
66
class LogtoUserInfoResponse {
77
@JsonKey(name: 'sub', required: true, disallowNullValue: true)
88
final String sub;
99
@JsonKey(name: 'username')
1010
final String? username;
1111
@JsonKey(name: 'name')
1212
final String? name;
13-
@JsonKey(name: 'avatar')
14-
final String? avatar;
15-
@JsonKey(name: 'role_names')
16-
final List<String>? roleNames;
13+
@JsonKey(name: 'picture')
14+
final String? picture;
15+
@JsonKey(name: 'email')
16+
final String? email;
17+
@JsonKey(name: 'email_verified')
18+
final bool? emailVerified;
19+
@JsonKey(name: 'phone_number')
20+
final String? phoneNumber;
21+
@JsonKey(name: 'phone_number_verified')
22+
final bool? phoneNumberVerified;
23+
@JsonKey(name: 'custom_data')
24+
final Map<String, dynamic>? customData;
25+
@JsonKey(name: 'identities')
26+
final Map<String, dynamic>? identities;
1727

18-
LogtoUserInfoResponse(
19-
{required this.sub,
20-
this.username,
21-
this.name,
22-
this.avatar,
23-
this.roleNames});
28+
LogtoUserInfoResponse({
29+
required this.sub,
30+
this.username,
31+
this.name,
32+
this.picture,
33+
this.email,
34+
this.emailVerified,
35+
this.phoneNumber,
36+
this.phoneNumberVerified,
37+
this.customData,
38+
this.identities,
39+
});
2440

2541
factory LogtoUserInfoResponse.fromJson(Map<String, dynamic> json) =>
2642
_$LogtoUserInfoResponseFromJson(json);

lib/src/interfaces/logto_user_info_response.g.dart

Lines changed: 29 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/logto_core_test.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ void main() {
137137
expect(result.refreshToken, mockRefreshTokenResponse['refresh_token']);
138138
});
139139

140-
test('Fetch UserInfo', () async {
140+
test('Fetch UserInfo should return all valid fields', () async {
141141
const String endpoint = '/oidc/me';
142142
const String accessToken = 'access_token';
143143

@@ -154,5 +154,15 @@ void main() {
154154

155155
expect(interceptor.isDone, true);
156156
expect(result.sub, mockUserInfoResponse['sub']);
157+
expect(result.name, mockUserInfoResponse['name']);
158+
expect(result.username, mockUserInfoResponse['username']);
159+
expect(result.picture, mockUserInfoResponse['picture']);
160+
expect(result.customData, mockUserInfoResponse['custom_data']);
161+
expect(result.email, mockUserInfoResponse['email']);
162+
expect(result.emailVerified, mockUserInfoResponse['email_verified']);
163+
expect(result.phoneNumber, mockUserInfoResponse['phone_number']);
164+
expect(result.phoneNumberVerified,
165+
mockUserInfoResponse['phone_number_verified']);
166+
expect(result.identities, mockUserInfoResponse['identities']);
157167
});
158168
}

test/mocks/responses.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ const Map<String, dynamic> mockUserInfoResponse = {
3030
"sub": "foo",
3131
"username": "username",
3232
"name": "name",
33-
"avatar": "http://avatar.png",
34-
"role_names": ['admin']
33+
"picture": "http://avatar.png",
34+
"email": "foo@logto.io",
35+
"email_verified": true,
36+
"phone_number": "123456789",
37+
"phone_number_verified": true,
38+
"custom_data": {},
39+
"identities": {
40+
"google": {"id": "google_id", "email": "foo@google.com"},
41+
},
42+
"user_roles": ["user"]
3543
};

0 commit comments

Comments
 (0)