@capgo/capacitor-social-login 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CapgoCapacitorSocialLogin.podspec +21 -0
  2. package/Package.swift +37 -0
  3. package/README.md +457 -0
  4. package/android/build.gradle +64 -0
  5. package/android/src/main/AndroidManifest.xml +4 -0
  6. package/android/src/main/java/ee/forgr/capacitor/social/login/AppleProvider.java +376 -0
  7. package/android/src/main/java/ee/forgr/capacitor/social/login/FacebookProvider.java +175 -0
  8. package/android/src/main/java/ee/forgr/capacitor/social/login/GoogleProvider.java +305 -0
  9. package/android/src/main/java/ee/forgr/capacitor/social/login/SocialLoginPlugin.java +161 -0
  10. package/android/src/main/java/ee/forgr/capacitor/social/login/helpers/JsonHelper.java +18 -0
  11. package/android/src/main/java/ee/forgr/capacitor/social/login/helpers/SocialProvider.java +13 -0
  12. package/android/src/main/res/.gitkeep +0 -0
  13. package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
  14. package/android/src/main/res/layout/dialog_custom_layout.xml +43 -0
  15. package/android/src/main/res/values/styles.xml +14 -0
  16. package/dist/docs.json +613 -0
  17. package/dist/esm/definitions.d.ts +191 -0
  18. package/dist/esm/definitions.js +2 -0
  19. package/dist/esm/definitions.js.map +1 -0
  20. package/dist/esm/index.d.ts +4 -0
  21. package/dist/esm/index.js +7 -0
  22. package/dist/esm/index.js.map +1 -0
  23. package/dist/esm/web.d.ts +17 -0
  24. package/dist/esm/web.js +29 -0
  25. package/dist/esm/web.js.map +1 -0
  26. package/dist/plugin.cjs.js +43 -0
  27. package/dist/plugin.cjs.js.map +1 -0
  28. package/dist/plugin.js +46 -0
  29. package/dist/plugin.js.map +1 -0
  30. package/ios/Sources/SocialLoginPlugin/AppleProvider.swift +516 -0
  31. package/ios/Sources/SocialLoginPlugin/FacebookProvider.swift +108 -0
  32. package/ios/Sources/SocialLoginPlugin/GoogleProvider.swift +165 -0
  33. package/ios/Sources/SocialLoginPlugin/SocialLoginPlugin.swift +322 -0
  34. package/ios/Tests/SocialLoginPluginTests/SocialLoginPluginTests.swift +15 -0
  35. package/package.json +87 -0
@@ -0,0 +1,516 @@
1
+ import Foundation
2
+ import AuthenticationServices
3
+ import Alamofire
4
+
5
+ struct AppleProviderResponse {
6
+ // let user: String
7
+ let identityToken: String
8
+ }
9
+
10
+ // Define the Decodable structs for the response
11
+ struct TokenResponse: Decodable {
12
+ let access_token: String?
13
+ let expires_in: Int?
14
+ let refresh_token: String?
15
+ let id_token: String?
16
+ }
17
+
18
+ // Define the custom error enum
19
+ enum AppleProviderError: Error {
20
+ case userDataSerializationError
21
+ case responseError(Error)
22
+ case invalidResponseCode(statusCode: Int)
23
+ case jsonParsingError
24
+ case specificJsonWritingError(Error)
25
+ case noLocationHeader
26
+ case pathComponentsNotFound
27
+ case successPathComponentNotProvided
28
+ case backendDidNotReturnSuccess(successValue: String)
29
+ case missingAccessToken
30
+ case missingExpiresIn
31
+ case missingRefreshToken
32
+ case missingIdToken
33
+ case missingUserId
34
+ case unknownError
35
+ case invalidIdToken
36
+ }
37
+
38
+ // Implement LocalizedError for AppleProviderError
39
+ extension AppleProviderError: LocalizedError {
40
+ var errorDescription: String? {
41
+ switch self {
42
+ case .userDataSerializationError:
43
+ return NSLocalizedString("Error converting user data to JSON string.", comment: "")
44
+ case .responseError(let error):
45
+ return NSLocalizedString("Response error: \(error.localizedDescription)", comment: "")
46
+ case .invalidResponseCode(let statusCode):
47
+ return NSLocalizedString("Invalid response code: \(statusCode).", comment: "")
48
+ case .noLocationHeader:
49
+ return NSLocalizedString("No Location header found in the redirect response.", comment: "")
50
+ case .pathComponentsNotFound:
51
+ return NSLocalizedString("Path components not found.", comment: "")
52
+ case .successPathComponentNotProvided:
53
+ return NSLocalizedString("Success path component not provided.", comment: "")
54
+ case .backendDidNotReturnSuccess(let successValue):
55
+ return NSLocalizedString("Backend did not return success=true, it returned success=\(successValue).", comment: "")
56
+ case .jsonParsingError:
57
+ return NSLocalizedString("Error parsing JSON response.", comment: "")
58
+ case .specificJsonWritingError(let error):
59
+ return NSLocalizedString("Error writing JSON. Error: \(error)", comment: "")
60
+ case .unknownError:
61
+ return NSLocalizedString("An unknown error occurred.", comment: "")
62
+ case .missingAccessToken:
63
+ return NSLocalizedString("Access token not found in response.", comment: "")
64
+ case .missingExpiresIn:
65
+ return NSLocalizedString("ExpiresIn not found in response.", comment: "")
66
+ case .missingRefreshToken:
67
+ return NSLocalizedString("Refresh token not found in response.", comment: "")
68
+ case .missingIdToken:
69
+ return NSLocalizedString("ID token not found in response.", comment: "")
70
+ case .missingUserId:
71
+ return NSLocalizedString("User ID not found in ID token.", comment: "")
72
+ case .invalidIdToken:
73
+ return NSLocalizedString("Invalid ID token format.", comment: "")
74
+ }
75
+ }
76
+ }
77
+
78
+ class AppleProvider: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
79
+ private var clientId: String?
80
+ private var completion: ((Result<AppleProviderResponse, Error>) -> Void)?
81
+
82
+ // Instance variables
83
+ var idToken: String?
84
+ var refreshToken: String?
85
+ var accessToken: String?
86
+
87
+ private let TOKEN_URL = "https://appleid.apple.com/auth/token"
88
+ private let SHARED_PREFERENCE_NAME = "AppleProviderSharedPrefs_0eda2642"
89
+ private var redirectUrl = ""
90
+
91
+ func initialize(clientId: String, redirectUrl: String) {
92
+ self.clientId = clientId
93
+ self.redirectUrl = redirectUrl
94
+
95
+ do {
96
+ try retrieveState()
97
+ } catch {
98
+ print("retrieveState error: \(error)")
99
+ }
100
+ }
101
+
102
+ func persistState(idToken: String, refreshToken: String, accessToken: String) throws {
103
+ // Create a dictionary to represent the JSON object
104
+ let object: [String: String] = [
105
+ "idToken": idToken,
106
+ "refreshToken": refreshToken,
107
+ "accessToken": accessToken
108
+ ]
109
+
110
+ // Assign to instance variables
111
+ self.idToken = idToken
112
+ self.refreshToken = refreshToken
113
+ self.accessToken = accessToken
114
+
115
+ // Convert the object to JSON data
116
+ let jsonData = try JSONSerialization.data(withJSONObject: object, options: [])
117
+
118
+ // Convert JSON data to a string for logging
119
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
120
+ // Log the object
121
+ print("Apple persistState: \(jsonString)")
122
+
123
+ // Save the JSON string to UserDefaults or use your helper method
124
+ UserDefaults.standard.set(jsonString, forKey: SHARED_PREFERENCE_NAME)
125
+ } else {
126
+ print("Error converting JSON data to String")
127
+ }
128
+ }
129
+
130
+ func retrieveState() throws {
131
+ // Retrieve the JSON string from persistent storage
132
+ guard let jsonString = UserDefaults.standard.string(forKey: SHARED_PREFERENCE_NAME) else {
133
+ print("No saved state found")
134
+ return
135
+ }
136
+
137
+ // Convert JSON string to Data
138
+ guard let jsonData = jsonString.data(using: .utf8) else {
139
+ print("Error converting JSON string to Data")
140
+ return
141
+ }
142
+
143
+ // Parse the JSON data
144
+ guard let object = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] else {
145
+ print("Error parsing JSON data")
146
+ return
147
+ }
148
+
149
+ // Extract tokens
150
+ guard let idToken = object["idToken"],
151
+ let refreshToken = object["refreshToken"],
152
+ let accessToken = object["accessToken"] else {
153
+ print("Error: Missing tokens in retrieved data")
154
+ return
155
+ }
156
+
157
+ // Assign to instance variables
158
+ self.idToken = idToken
159
+ self.refreshToken = refreshToken
160
+ self.accessToken = accessToken
161
+
162
+ // Log the retrieved object
163
+ print("Apple retrieveState: \(object)")
164
+ }
165
+
166
+ func login(payload: [String: Any], completion: @escaping (Result<AppleProviderResponse, Error>) -> Void) {
167
+ guard let clientId = clientId else {
168
+ completion(.failure(NSError(domain: "AppleProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "Client ID not set"])))
169
+ return
170
+ }
171
+
172
+ self.completion = completion
173
+
174
+ let appleIDProvider = ASAuthorizationAppleIDProvider()
175
+ let request = appleIDProvider.createRequest()
176
+ request.requestedScopes = [.fullName, .email]
177
+
178
+ let authorizationController = ASAuthorizationController(authorizationRequests: [request])
179
+ authorizationController.delegate = self
180
+ authorizationController.presentationContextProvider = self
181
+ authorizationController.performRequests()
182
+ }
183
+
184
+ func logout(completion: @escaping (Result<Void, Error>) -> Void) {
185
+ // we check only idtoken, because with apple, refresh token MIGHT not be set
186
+ if self.idToken == nil || ((self.idToken?.isEmpty) == true) {
187
+
188
+ completion(.failure(NSError(domain: "AppleProvider", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not logged in; Cannot logout"])))
189
+ return
190
+ }
191
+
192
+ self.idToken = nil
193
+ self.refreshToken = nil
194
+ self.accessToken = nil
195
+
196
+ UserDefaults.standard.removeObject(forKey: SHARED_PREFERENCE_NAME)
197
+ completion(.success(()))
198
+ return
199
+ }
200
+
201
+ func getCurrentUser(completion: @escaping (Result<AppleProviderResponse?, Error>) -> Void) {
202
+ let appleIDProvider = ASAuthorizationAppleIDProvider()
203
+ appleIDProvider.getCredentialState(forUserID: "currentUserIdentifier") { (credentialState, error) in
204
+ if let error = error {
205
+ completion(.failure(error))
206
+ return
207
+ }
208
+
209
+ switch credentialState {
210
+ case .authorized:
211
+ // User is authorized, you might want to fetch more details here
212
+ completion(.success(nil))
213
+ case .revoked, .notFound:
214
+ completion(.success(nil))
215
+ @unknown default:
216
+ completion(.failure(NSError(domain: "AppleProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unknown credential state"])))
217
+ }
218
+ }
219
+ }
220
+
221
+ func refresh(completion: @escaping (Result<Void, Error>) -> Void) {
222
+ // Apple doesn't provide a refresh method
223
+ completion(.success(()))
224
+ }
225
+
226
+ // MARK: - ASAuthorizationControllerDelegate
227
+
228
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
229
+ if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
230
+ let userIdentifier = appleIDCredential.user
231
+ let fullName = appleIDCredential.fullName
232
+ let email = appleIDCredential.email
233
+
234
+ // let response = AppleProviderResponse(
235
+ // user: userIdentifier,
236
+ // email: email,
237
+ // givenName: fullName?.givenName,
238
+ // familyName: fullName?.familyName,
239
+ // identityToken: String(data: appleIDCredential.identityToken ?? Data(), encoding: .utf8) ?? "",
240
+ // authorizationCode: String(data: appleIDCredential.authorizationCode ?? Data(), encoding: .utf8) ?? ""
241
+ // )
242
+
243
+ let errorCompletion: ((Result<AppleProviderResponse, AppleProviderError>) -> Void) = { result in
244
+ do {
245
+ let finalResult = try result.get()
246
+ self.completion?(.success(finalResult))
247
+ } catch {
248
+ self.completion?(.failure(error))
249
+ }
250
+ }
251
+
252
+ let authorizationCode = String(data: appleIDCredential.authorizationCode ?? Data(), encoding: .utf8) ?? ""
253
+ let identityToken = String(data: appleIDCredential.identityToken ?? Data(), encoding: .utf8) ?? ""
254
+
255
+ if !self.redirectUrl.isEmpty {
256
+ let firstName = fullName?.givenName ?? "Jhon"
257
+ let lastName = fullName?.familyName ?? "Doe"
258
+
259
+ if let _ = fullName?.givenName {
260
+ sendRequest(code: authorizationCode, identityToken: identityToken, email: email ?? "", firstName: firstName, lastName: lastName, completion: errorCompletion, skipUser: false)
261
+ } else {
262
+ sendRequest(code: authorizationCode, identityToken: identityToken, email: email ?? "", firstName: firstName, lastName: lastName, completion: errorCompletion, skipUser: true)
263
+ }
264
+ } else {
265
+ do {
266
+ try self.persistState(idToken: identityToken, refreshToken: "", accessToken: "")
267
+ let appleResponse = AppleProviderResponse(identityToken: identityToken)
268
+ completion?(.success(appleResponse))
269
+ return
270
+ } catch {
271
+ completion?(.failure(AppleProviderError.specificJsonWritingError(error)))
272
+ return
273
+ }
274
+ }
275
+ // completion?(.success(response))
276
+ }
277
+ }
278
+
279
+ // identityToken is the JWT generated by apple
280
+ func sendRequest(
281
+ code: String,
282
+ identityToken: String,
283
+ email: String,
284
+ firstName: String,
285
+ lastName: String,
286
+ completion: @escaping ((Result<AppleProviderResponse, AppleProviderError>) -> Void),
287
+ skipUser: Bool
288
+ ) {
289
+ // Prepare the parameters
290
+ var parameters: [String: String] = [
291
+ "code": code
292
+ ]
293
+
294
+ if !skipUser {
295
+ let user: [String: Any] = [
296
+ "email": email,
297
+ "name": [
298
+ "firstName": firstName,
299
+ "lastName": lastName
300
+ ]
301
+ ]
302
+
303
+ // Convert the user dictionary to a JSON string
304
+ guard let userData = try? JSONSerialization.data(withJSONObject: user, options: []),
305
+ let userJSONString = String(data: userData, encoding: .utf8) else {
306
+ print("Error converting user data to JSON string")
307
+ return
308
+ }
309
+
310
+ parameters["user"] = userJSONString
311
+ }
312
+
313
+ // Send the POST request
314
+ AF.request(
315
+ self.redirectUrl,
316
+ method: .post,
317
+ parameters: parameters,
318
+ encoding: URLEncoding.default,
319
+ headers: [ "ios-plugin-version": "0.0.0" ]
320
+ )
321
+ .redirect(using: Redirector(behavior: .doNotFollow))
322
+ .response { response in
323
+ // Access the HTTPURLResponse
324
+ if let httpResponse = response.response {
325
+ print("Status Code: \(httpResponse.statusCode)")
326
+
327
+ // Check if the response is a redirect
328
+ if (300...399).contains(httpResponse.statusCode) {
329
+ if let location = httpResponse.headers.value(for: "Location") {
330
+ print("Redirect Location: \(location)")
331
+
332
+ // Parse the redirect URL
333
+ if let redirectURL = URL(string: location),
334
+ let urlComponents = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false),
335
+ let pathComponents = urlComponents.queryItems {
336
+
337
+ print("Query items: \(String(describing: urlComponents.queryItems))")
338
+
339
+ // there are 4 main ways this can go:
340
+ // 1. it provides the "code" and we fetch apple servers in order to get the JWT (yuck)
341
+ // 2. It doesn't provide the code but it provides access_token, refresh_token, id_token
342
+ // 3. It doesn't provide a thing, reuse the JWT returned by internal apple login
343
+ // 4. it returns a fail
344
+
345
+ guard let success = (pathComponents.filter { $0.name == "success" }.first?.value) else {
346
+ completion(.failure(.successPathComponentNotProvided))
347
+ return
348
+ }
349
+
350
+ if success != "true" {
351
+ completion(.failure(.backendDidNotReturnSuccess(successValue: success)))
352
+ return
353
+ }
354
+
355
+ if let code = (pathComponents.filter { $0.name == "code" }.first?.value),
356
+ let clientSecret = (pathComponents.filter { $0.name == "client_secret" }.first?.value) {
357
+
358
+ self.exchangeCodeForTokens(clientSecret: clientSecret, code: code, completion: completion)
359
+ return
360
+ }
361
+
362
+ if let accessToken = (pathComponents.filter { $0.name == "access_token" }.first?.value),
363
+ let refreshToken = (pathComponents.filter { $0.name == "refresh_token" }.first?.value),
364
+ let idToken = (pathComponents.filter { $0.name == "id_token" }.first?.value) {
365
+
366
+ do {
367
+ try self.persistState(idToken: idToken, refreshToken: refreshToken, accessToken: accessToken)
368
+ let appleResponse = AppleProviderResponse(identityToken: idToken)
369
+ completion(.success(appleResponse))
370
+ return
371
+ } catch {
372
+ completion(.failure(.specificJsonWritingError(error)))
373
+ return
374
+ }
375
+ }
376
+
377
+ if (pathComponents.filter { $0.name == "ios_no_code" }).first != nil {
378
+ // identityToken provided by apple
379
+ let appleResponse = AppleProviderResponse(identityToken: identityToken)
380
+
381
+ do {
382
+ try self.persistState(idToken: identityToken, refreshToken: "", accessToken: "")
383
+ completion(.success(appleResponse))
384
+ return
385
+ } catch {
386
+ completion(.failure(AppleProviderError.specificJsonWritingError(error)))
387
+ return
388
+ }
389
+ }
390
+
391
+ } else {
392
+ completion(.failure(.pathComponentsNotFound))
393
+ print("Path components not found")
394
+ return
395
+ }
396
+ } else {
397
+ completion(.failure(.noLocationHeader))
398
+ print("No Location header found in the redirect response")
399
+ return
400
+ }
401
+ } else {
402
+ // Handle non-redirect responses
403
+ if let data = response.data,
404
+ let responseString = String(data: data, encoding: .utf8) {
405
+ print("Response: \(responseString)")
406
+ } else {
407
+ print("No response data received")
408
+ }
409
+
410
+ completion(.failure(.invalidResponseCode(statusCode: httpResponse.statusCode)))
411
+ }
412
+ } else if let error = response.error {
413
+ completion(.failure(.responseError(error)))
414
+ print("Error: \(error)")
415
+ }
416
+ }
417
+ }
418
+
419
+ func exchangeCodeForTokens(clientSecret: String, code: String, completion: @escaping ((Result<AppleProviderResponse, AppleProviderError>) -> Void)) {
420
+ // Prepare the parameters
421
+ let parameters: [String: String] = [
422
+ "client_id": Bundle.main.bundleIdentifier ?? "", // TODO: implement better handling when client_id = null
423
+ "client_secret": clientSecret, // Implement this function to generate the client secret
424
+ "code": code,
425
+ "grant_type": "authorization_code"
426
+ ]
427
+
428
+ AF.request(
429
+ TOKEN_URL,
430
+ method: .post,
431
+ parameters: parameters,
432
+ encoder: URLEncodedFormParameterEncoder.default
433
+ )
434
+ .validate(statusCode: 200..<300) // Ensure the response status code is in the 200-299 range
435
+ .responseDecodable(of: TokenResponse.self) { response in
436
+ switch response.result {
437
+ case .success(let tokenResponse):
438
+ // Extract tokens from the response
439
+ guard let accessToken = tokenResponse.access_token else {
440
+ completion(.failure(.missingAccessToken))
441
+ return
442
+ }
443
+ guard let expiresIn = tokenResponse.expires_in else {
444
+ completion(.failure(.missingExpiresIn))
445
+ return
446
+ }
447
+ guard let refreshToken = tokenResponse.refresh_token else {
448
+ completion(.failure(.missingRefreshToken))
449
+ return
450
+ }
451
+ guard let idToken = tokenResponse.id_token else {
452
+ completion(.failure(.missingIdToken))
453
+ return
454
+ }
455
+
456
+ // Decode the ID token to extract the user ID
457
+ let idTokenParts = idToken.split(separator: ".")
458
+ if idTokenParts.count >= 2 {
459
+ let encodedUserID = String(idTokenParts[1])
460
+
461
+ // Pad the base64 string if necessary
462
+ let remainder = encodedUserID.count % 4
463
+ var base64String = encodedUserID
464
+ if remainder > 0 {
465
+ base64String += String(repeating: "=", count: 4 - remainder)
466
+ }
467
+
468
+ // Decode the base64 string
469
+ if let decodedData = Data(base64Encoded: base64String, options: []),
470
+ let userData = try? JSONSerialization.jsonObject(with: decodedData, options: []) as? [String: Any],
471
+ let userId = userData["sub"] as? String {
472
+ // Create the response object
473
+ let appleResponse = AppleProviderResponse(identityToken: idToken)
474
+
475
+ // Log the tokens (replace with your logging mechanism)
476
+ print("Apple Access Token is: \(accessToken)")
477
+ print("Expires in: \(expiresIn)")
478
+ print("Refresh token: \(refreshToken)")
479
+ print("ID Token: \(idToken)")
480
+ print("Apple User ID: \(userId)")
481
+
482
+ do {
483
+ try self.persistState(idToken: idToken, refreshToken: refreshToken, accessToken: accessToken)
484
+ } catch {
485
+ completion(.failure(.specificJsonWritingError(error)))
486
+ }
487
+
488
+ // Call the completion handler with the response
489
+ completion(.success(appleResponse))
490
+ } else {
491
+ completion(.failure(.missingUserId))
492
+ }
493
+ } else {
494
+ completion(.failure(.invalidIdToken))
495
+ }
496
+ case .failure(let error):
497
+ if let statusCode = response.response?.statusCode {
498
+ print("error", response.debugDescription)
499
+ completion(.failure(.invalidResponseCode(statusCode: statusCode)))
500
+ } else {
501
+ completion(.failure(.responseError(error)))
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
508
+ completion?(.failure(error))
509
+ }
510
+
511
+ // MARK: - ASAuthorizationControllerPresentationContextProviding
512
+
513
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
514
+ return UIApplication.shared.windows.first!
515
+ }
516
+ }
@@ -0,0 +1,108 @@
1
+ import Foundation
2
+ import FBSDKLoginKit
3
+
4
+ struct FacebookLoginResponse {
5
+ let accessToken: [String: Any]
6
+ let profile: [String: Any]
7
+ }
8
+
9
+ class FacebookProvider {
10
+ private let loginManager = LoginManager()
11
+ private let dateFormatter = ISO8601DateFormatter()
12
+
13
+ init() {
14
+ if #available(iOS 11.2, *) {
15
+ dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
16
+ } else {
17
+ dateFormatter.formatOptions = [.withInternetDateTime]
18
+ }
19
+ }
20
+
21
+ private func dateToJS(_ date: Date) -> String {
22
+ return dateFormatter.string(from: date)
23
+ }
24
+
25
+ func initialize() {
26
+ // No initialization required for FacebookProvider
27
+ }
28
+
29
+ func login(payload: [String: Any], completion: @escaping (Result<FacebookLoginResponse, Error>) -> Void) {
30
+ guard let permissions = payload["permissions"] as? [String] else {
31
+ completion(.failure(NSError(domain: "FacebookProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "Missing permissions"])))
32
+ return
33
+ }
34
+
35
+ DispatchQueue.main.async {
36
+ self.loginManager.logIn(permissions: permissions, from: nil) { result, error in
37
+ if let error = error {
38
+ completion(.failure(error))
39
+ return
40
+ }
41
+
42
+ guard let result = result, !result.isCancelled else {
43
+ completion(.failure(NSError(domain: "FacebookProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "Login cancelled"])))
44
+ return
45
+ }
46
+
47
+ let accessToken = result.token
48
+ let response = FacebookLoginResponse(
49
+ accessToken: [
50
+ "applicationID": accessToken?.appID ?? "",
51
+ "declinedPermissions": accessToken?.declinedPermissions.map { $0.name } ?? [],
52
+ "expirationDate": accessToken?.expirationDate ?? Date(),
53
+ "isExpired": accessToken?.isExpired ?? false,
54
+ "refreshDate": accessToken?.refreshDate ?? Date(),
55
+ "permissions": accessToken?.permissions.map { $0.name } ?? [],
56
+ "tokenString": accessToken?.tokenString ?? "",
57
+ "userID": accessToken?.userID ?? ""
58
+ ],
59
+ profile: [:]
60
+ )
61
+
62
+ completion(.success(response))
63
+ }
64
+ }
65
+ }
66
+
67
+ func logout(completion: @escaping (Result<Void, Error>) -> Void) {
68
+ loginManager.logOut()
69
+ completion(.success(()))
70
+ }
71
+
72
+ func getCurrentUser(completion: @escaping (Result<[String: Any]?, Error>) -> Void) {
73
+ if let accessToken = AccessToken.current {
74
+ let response: [String: Any] = [
75
+ "accessToken": [
76
+ "applicationID": accessToken.appID,
77
+ "declinedPermissions": accessToken.declinedPermissions.map { $0.name },
78
+ "expirationDate": accessToken.expirationDate,
79
+ "isExpired": accessToken.isExpired,
80
+ "refreshDate": accessToken.refreshDate,
81
+ "permissions": accessToken.permissions.map { $0.name },
82
+ "tokenString": accessToken.tokenString,
83
+ "userID": accessToken.userID
84
+ ],
85
+ "profile": [:]
86
+ ]
87
+ completion(.success(response))
88
+ } else {
89
+ completion(.success(nil))
90
+ }
91
+ }
92
+
93
+ func refresh(viewController: UIViewController?, completion: @escaping (Result<Void, Error>) -> Void) {
94
+ DispatchQueue.main.async {
95
+ if let token = AccessToken.current, !token.isExpired {
96
+ completion(.success(()))
97
+ } else {
98
+ self.loginManager.reauthorizeDataAccess(from: viewController!) { loginResult, error in
99
+ if let _ = loginResult?.token {
100
+ completion(.success(()))
101
+ } else {
102
+ completion(.failure(error ?? NSError(domain: "FacebookProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "Reauthorization failed"])))
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }