/
flutter_facebook_login.dart
358 lines (319 loc) · 12.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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
import 'dart:async';
import 'package:flutter/services.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
/// FacebookLogin facebookLogin = new FacebookLogin();
/// FacebookLoginResult result =
/// await facebookLogin.logInWithReadPermissions(['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 MethodChannel channel =
const 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.
///
/// Updating the login behavior won't do anything immediately; the value is
/// taken into account just before the login dialog is about to show.
set loginBehavior(FacebookLoginBehavior behavior) {
assert(behavior != null, 'The login behavior cannot be null.');
_loginBehavior = behavior;
}
/// Returns whether the user is currently logged in or not.
///
/// Convenience method for checking if the [currentAccessToken] is null.
Future<bool> get isLoggedIn async => await currentAccessToken != null;
/// 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 FacebookAccessToken accessToken = await facebookLogin.currentAccessToken;
///
/// if (accessToken != null) {
/// _fetchFacebookNewsFeed(accessToken);
/// } else {
/// _showLoginRequiredUI();
/// }
/// ```
///
/// If the user is not logged in, this returns null.
Future<FacebookAccessToken> get currentAccessToken async {
final Map<dynamic, dynamic> accessToken =
await channel.invokeMethod('getCurrentAccessToken');
if (accessToken == null) {
return null;
}
return new 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> logInWithReadPermissions(
List<String> permissions,
) async {
final Map<dynamic, dynamic> result =
await channel.invokeMethod('loginWithReadPermissions', {
'behavior': _currentLoginBehaviorAsString(),
'permissions': permissions,
});
return _deliverResult(
new FacebookLoginResult._(result.cast<String, dynamic>()));
}
/// Logs the user in with the requested publish permissions.
///
/// This will throw an exception from the native side if the [permissions]
/// list contains any permissions that are not classified as read permissions.
///
/// If called right after receiving a result from [logInWithReadPermissions],
/// this method may fail. It is recommended to call this method right before
/// needing a specific publish permission, in a context where it makes sense
/// to the user. For example, a good place to call this method would be when
/// the user is about to post something to Facebook by using your app.
///
/// Returns a [FacebookLoginResult] that contains relevant information about
/// the current login status. For sample code, see the [FacebookLogin] class-
/// level documentation.
Future<FacebookLoginResult> loginWithPublishPermissions(
List<String> permissions,
) async {
final Map<dynamic, dynamic> result =
await channel.invokeMethod('loginWithPublishPermissions', {
'behavior': _currentLoginBehaviorAsString(),
'permissions': permissions,
});
return _deliverResult(
new 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, when using [FacebookLoginBehavior.webOnly], the WKViewController
/// 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. This same issue is also present when using
/// [FacebookLoginBehavior.nativeWithFallback] in the case where the user
/// doesn't have a native Facebook app installed.
///
/// Using [FacebookLoginBehavior.webViewOnly] resolves this issue.
///
/// For more, see: https://github.com/roughike/flutter_facebook_login/issues/4
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 new 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 new Future.delayed(const Duration(milliseconds: 500), () => result);
}
}
/// Different behaviors for controlling how the Facebook Login dialog should
/// be presented.
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
? new 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 new 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;
/// Constructs a new access token instance from a [Map].
///
/// This is used mostly internally by this library, but could be useful if
/// storing the token locally by using the [toMap] method.
FacebookAccessToken.fromMap(Map<String, dynamic> map)
: token = map['token'],
userId = map['userId'],
expires = new DateTime.fromMillisecondsSinceEpoch(
map['expires'],
isUtc: true,
),
permissions = map['permissions'].cast<String>(),
declinedPermissions = map['declinedPermissions'].cast<String>();
/// Transforms this access token to a [Map].
///
/// This could be useful for encoding this access token as JSON and then
/// storing it locally.
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 &&
permissions == other.permissions &&
declinedPermissions == other.declinedPermissions;
@override
int get hashCode =>
token.hashCode ^
userId.hashCode ^
expires.hashCode ^
permissions.hashCode ^
declinedPermissions.hashCode;
}