This repository has been archived by the owner on Jul 8, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 331
/
flutter_facebook_login.dart
342 lines (304 loc) · 11.7 KB
/
flutter_facebook_login.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
import 'src/clock.dart';
/// FacebookLogin is a plugin for authenticating your users using the native
/// Android & iOS Facebook Login SDKs.
///
/// The login methods return a [FacebookLoginResult] that contains relevant
/// information about whether the user logged in, cancelled the login dialog,
/// or if the login flow resulted in an error.
///
/// For example, this sample code illustrates how to handle the different
/// cases:
///
/// ```dart
/// final facebookLogin = FacebookLogin();
/// final result =
/// await facebookLogin.logInWithPermissions(['email']);
///
/// switch (result.status) {
/// case FacebookLoginStatus.loggedIn:
/// _sendTokenToServer(result.accessToken.token);
/// _showLoggedInUI();
/// break;
/// case FacebookLoginStatus.cancelledByUser:
/// _showConvincingMessageOnUI();
/// break;
/// case FacebookLoginStatus.error:
/// _showErrorOnUI();
/// break;
/// }
///
/// Before using this plugin, some initial setup is required for the Android
/// and iOS clients. See the README for detailed instructions.
/// ```
class FacebookLogin {
static const channel = MethodChannel('com.roughike/flutter_facebook_login');
FacebookLoginBehavior _loginBehavior =
FacebookLoginBehavior.nativeWithFallback;
/// Controls how the login dialog should be presented.
///
/// For example, setting this to [FacebookLoginBehavior.webViewOnly] will
/// render the login dialog using a WebView.
///
/// NOTE: Updating the login behavior won't do anything immediately; the value
/// is taken into account just before the login dialog is about to show.
///
/// Ignored on iOS, as it's not supported by the iOS Facebook Login SDK anymore.
set loginBehavior(FacebookLoginBehavior behavior) {
assert(behavior != null, 'The login behavior cannot be null.');
_loginBehavior = behavior;
}
/// Returns whether the user is currently logged in and the access token is
/// still valid or not.
///
/// Convenience method for checking if the [currentAccessToken] is null and not
/// expired.
Future<bool> get isLoggedIn async =>
(await currentAccessToken)?.isValid() ?? false;
/// Retrieves the current access token for the application.
///
/// This could be useful for logging in the user automatically in the case
/// where you don't persist the access token in your Flutter app yourself.
///
/// For example:
///
/// ```dart
/// final accessToken = await facebookLogin.currentAccessToken;
///
/// if (accessToken != null && accessToken.isValid()) {
/// _fetchFacebookNewsFeed(accessToken);
/// } else {
/// _showLoginRequiredUI();
/// }
/// ```
///
/// NOTE: This might return an access token that has expired. If you need to be
/// sure that the token is still valid, call [isValid] on the access token.
Future<FacebookAccessToken> get currentAccessToken async {
final Map<dynamic, dynamic> accessToken =
await channel.invokeMethod('getCurrentAccessToken');
if (accessToken == null) {
return null;
}
return FacebookAccessToken.fromMap(accessToken.cast<String, dynamic>());
}
/// Logs the user in with the requested read permissions.
///
/// This will throw an exception from the native side if the [permissions]
/// list contains any permissions that are not classified as read permissions.
///
/// Returns a [FacebookLoginResult] that contains relevant information about
/// the current login status. For sample code, see the [FacebookLogin] class-
/// level documentation.
Future<FacebookLoginResult> logIn(
List<String> permissions,
) async {
final Map<dynamic, dynamic> result =
await channel.invokeMethod('logIn', {
'behavior': _currentLoginBehaviorAsString(),
'permissions': permissions,
});
return _deliverResult(
FacebookLoginResult._(result.cast<String, dynamic>()));
}
/// Logs the currently logged in user out.
///
/// NOTE: On iOS, this behaves in an unwanted way. As far the Login SDK is
/// concerned, the access token and session is cleared upon logging out.
/// However, ViewController managed by Safari remembers the user indefinitely.
///
/// This blocks the user from logging in with any other account than the one
/// they used the first time.
///
/// For more, see: https://github.com/facebook/facebook-swift-sdk/issues/215
Future<void> logOut() async => channel.invokeMethod('logOut');
String _currentLoginBehaviorAsString() {
assert(_loginBehavior != null, 'The login behavior was unexpectedly null.');
switch (_loginBehavior) {
case FacebookLoginBehavior.nativeWithFallback:
return 'nativeWithFallback';
case FacebookLoginBehavior.nativeOnly:
return 'nativeOnly';
case FacebookLoginBehavior.webOnly:
return 'webOnly';
case FacebookLoginBehavior.webViewOnly:
return 'webViewOnly';
}
throw StateError('Invalid login behavior.');
}
/// There's a weird bug where calling Navigator.push (or any similar method)
/// straight after getting a result from the method channel causes the app
/// to hang.
///
/// As a hack/workaround, we add a new task to the task queue with a slight
/// delay, using the [Future.delayed] constructor.
///
/// For more context, see this issue:
/// https://github.com/roughike/flutter_facebook_login/issues/14
Future<T> _deliverResult<T>(T result) {
return Future.delayed(const Duration(milliseconds: 500), () => result);
}
}
/// Different behaviors for controlling how the Facebook Login dialog should
/// be presented.
///
/// Ignored on iOS, as it's not supported by the iOS Facebook Login SDK anymore.
enum FacebookLoginBehavior {
/// Login dialog should be rendered by the native Android or iOS Facebook app.
///
/// If the user doesn't have a native Facebook app installed, this falls back
/// to using the web browser based auth dialog.
///
/// This is the default login behavior.
///
/// Might have logout issues on iOS; see the [FacebookLogin.logOut] documentation.
nativeWithFallback,
/// Login dialog should be rendered by the native Android or iOS Facebook app
/// only.
///
/// If the user hasn't installed the Facebook app on their device, the
/// login will fail when using this behavior.
///
/// On iOS, this behaves like the [nativeWithFallback] option. This is because
/// the iOS Facebook Login SDK doesn't support the native-only login.
nativeOnly,
/// Login dialog should be rendered by using a web browser.
///
/// Might have logout issues on iOS; see the [FacebookLogin.logOut] documentation.
webOnly,
/// Login dialog should be rendered by using a WebView.
webViewOnly,
}
/// The result when the Facebook login flow has completed.
///
/// The login methods always return an instance of this class, whether the
/// user logged in, cancelled or the login resulted in an error. To handle
/// the different possible scenarios, first see what the [status] is.
///
/// To see a comprehensive example on how to handle the different login
/// results, see the [FacebookLogin] class-level documentation.
class FacebookLoginResult {
/// The status after a Facebook login flow has completed.
///
/// This affects the [accessToken] and [errorMessage] variables and whether
/// they're available or not. If the user cancelled the login flow, both
/// [accessToken] and [errorMessage] are null.
final FacebookLoginStatus status;
/// The access token for using the Facebook APIs, obtained after the user has
/// successfully logged in.
///
/// Only available when the [status] equals [FacebookLoginStatus.loggedIn],
/// otherwise null.
final FacebookAccessToken accessToken;
/// The error message when the log in flow completed with an error.
///
/// Only available when the [status] equals [FacebookLoginStatus.error],
/// otherwise null.
final String errorMessage;
FacebookLoginResult._(Map<String, dynamic> map)
: status = _parseStatus(map['status']),
accessToken = map['accessToken'] != null
? FacebookAccessToken.fromMap(
map['accessToken'].cast<String, dynamic>(),
)
: null,
errorMessage = map['errorMessage'];
static FacebookLoginStatus _parseStatus(String status) {
switch (status) {
case 'loggedIn':
return FacebookLoginStatus.loggedIn;
case 'cancelledByUser':
return FacebookLoginStatus.cancelledByUser;
case 'error':
return FacebookLoginStatus.error;
}
throw StateError('Invalid status: $status');
}
}
/// The status after a Facebook login flow has completed.
enum FacebookLoginStatus {
/// The login was successful and the user is now logged in.
loggedIn,
/// The user cancelled the login flow, usually by closing the Facebook
/// login dialog.
cancelledByUser,
/// The Facebook login completed with an error and the user couldn't log
/// in for some reason.
error,
}
/// The access token for using Facebook APIs.
///
/// Includes the token itself, along with useful metadata about it, such as the
/// associated user id, expiration date and permissions that the token contains.
class FacebookAccessToken {
/// The access token returned by the Facebook login, which can be used to
/// access Facebook APIs.
final String token;
/// The id for the user that is associated with this access token.
final String userId;
/// The date when this access token expires.
final DateTime expires;
/// The list of accepted permissions associated with this access token.
///
/// These are the permissions that were requested with last login, and which
/// the user approved. If permissions have changed since the last login, this
/// list might be outdated.
final List<String> permissions;
/// The list of declined permissions associated with this access token.
///
/// These are the permissions that were requested, but the user didn't
/// approve. Similarly to [permissions], this list might be outdated if these
/// permissions have changed since the last login.
final List<String> declinedPermissions;
/// Is this access token expired or not?
///
/// If the access token has not been expired yet, returns true. Otherwise,
/// returns false.
bool isValid() => Clock.now().isBefore(expires);
/// Constructs a access token instance from a [Map].
///
/// This is used mostly internally by this library.
FacebookAccessToken.fromMap(Map<String, dynamic> map)
: token = map['token'],
userId = map['userId'],
expires = DateTime.fromMillisecondsSinceEpoch(
map['expires'],
isUtc: true,
),
permissions = map['permissions'].cast<String>(),
declinedPermissions = map['declinedPermissions'].cast<String>();
/// Transforms this access token to a [Map].
///
/// This is used mostly internally by this library.
Map<String, dynamic> toMap() {
return <String, dynamic>{
'token': token,
'userId': userId,
'expires': expires.millisecondsSinceEpoch,
'permissions': permissions,
'declinedPermissions': declinedPermissions,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FacebookAccessToken &&
runtimeType == other.runtimeType &&
token == other.token &&
userId == other.userId &&
expires == other.expires &&
const IterableEquality().equals(permissions, other.permissions) &&
const IterableEquality().equals(
declinedPermissions,
other.declinedPermissions,
);
@override
int get hashCode =>
token.hashCode ^
userId.hashCode ^
expires.hashCode ^
permissions.hashCode ^
declinedPermissions.hashCode;
}