@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.
- package/CapgoCapacitorSocialLogin.podspec +21 -0
- package/Package.swift +37 -0
- package/README.md +457 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/AppleProvider.java +376 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/FacebookProvider.java +175 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/GoogleProvider.java +305 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/SocialLoginPlugin.java +161 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/helpers/JsonHelper.java +18 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/helpers/SocialProvider.java +13 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
- package/android/src/main/res/layout/dialog_custom_layout.xml +43 -0
- package/android/src/main/res/values/styles.xml +14 -0
- package/dist/docs.json +613 -0
- package/dist/esm/definitions.d.ts +191 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +17 -0
- package/dist/esm/web.js +29 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +43 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +46 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/SocialLoginPlugin/AppleProvider.swift +516 -0
- package/ios/Sources/SocialLoginPlugin/FacebookProvider.swift +108 -0
- package/ios/Sources/SocialLoginPlugin/GoogleProvider.swift +165 -0
- package/ios/Sources/SocialLoginPlugin/SocialLoginPlugin.swift +322 -0
- package/ios/Tests/SocialLoginPluginTests/SocialLoginPluginTests.swift +15 -0
- 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
|
+
}
|