@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,165 @@
1
+ import Foundation
2
+ import GoogleSignIn
3
+
4
+ class GoogleProvider {
5
+ var configuration: GIDConfiguration!
6
+ var forceAuthCode: Bool = false
7
+ var additionalScopes: [String]!
8
+
9
+ func initialize(clientId: String) {
10
+ let serverClientId = getServerClientIdValue()
11
+ configuration = GIDConfiguration(clientID: clientId, serverClientID: serverClientId)
12
+
13
+ let defaultGrantedScopes = ["email", "profile", "openid"]
14
+ additionalScopes = []
15
+
16
+ forceAuthCode = false
17
+ }
18
+
19
+ func login(payload: [String: Any], completion: @escaping (Result<GoogleLoginResponse, Error>) -> Void) {
20
+ DispatchQueue.main.async {
21
+ if GIDSignIn.sharedInstance.hasPreviousSignIn() && !self.forceAuthCode {
22
+ GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
23
+ if let error = error {
24
+ completion(.failure(error))
25
+ return
26
+ }
27
+ completion(.success(self.createLoginResponse(user: user!)))
28
+ }
29
+ } else {
30
+ guard let presentingVc = UIApplication.shared.windows.first?.rootViewController else {
31
+ completion(.failure(NSError(domain: "GoogleProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "No presenting view controller found"])))
32
+ return
33
+ }
34
+
35
+ GIDSignIn.sharedInstance.signIn(
36
+ withPresenting: presentingVc,
37
+ hint: nil,
38
+ additionalScopes: self.additionalScopes
39
+ ) { result, error in
40
+ if let error = error {
41
+ completion(.failure(error))
42
+ return
43
+ }
44
+ guard let result = result else {
45
+ completion(.failure(NSError(domain: "GoogleProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "No result returned"])))
46
+ return
47
+ }
48
+ completion(.success(self.createLoginResponse(user: result.user)))
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ func logout(completion: @escaping (Result<Void, Error>) -> Void) {
55
+ DispatchQueue.main.async {
56
+ GIDSignIn.sharedInstance.signOut()
57
+ completion(.success(()))
58
+ }
59
+ }
60
+
61
+ func isLoggedIn(completion: @escaping (Result<Bool, Error>) -> Void) {
62
+ DispatchQueue.main.async {
63
+ if GIDSignIn.sharedInstance.currentUser != nil {
64
+ completion(.success(true))
65
+ return
66
+ }
67
+ if GIDSignIn.sharedInstance.hasPreviousSignIn() {
68
+ GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
69
+ if let error = error {
70
+ completion(.failure(error))
71
+ return
72
+ }
73
+ completion(.success(user != nil))
74
+ }
75
+ } else {
76
+ completion(.success(false))
77
+ }
78
+ }
79
+ }
80
+
81
+ func getAuthorizationCode(completion: @escaping (Result<String, Error>) -> Void) {
82
+ DispatchQueue.main.async {
83
+ if let currentUser = GIDSignIn.sharedInstance.currentUser, let idToken = currentUser.idToken?.tokenString {
84
+ completion(.success(idToken))
85
+ return
86
+ }
87
+ if GIDSignIn.sharedInstance.hasPreviousSignIn() {
88
+ GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
89
+ if let error = error {
90
+ completion(.failure(error))
91
+ return
92
+ }
93
+ if let user = user, let idToken = user.idToken?.tokenString {
94
+ completion(.success(idToken))
95
+ return
96
+ }
97
+ completion(.failure(NSError(domain: "GoogleProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "AuthorizationCode not found for google login"])))
98
+ }
99
+ } else {
100
+ completion(.failure(NSError(domain: "GoogleProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "AuthorizationCode not found for google login"])))
101
+ }
102
+ }
103
+ }
104
+
105
+ func getCurrentUser(completion: @escaping (Result<GoogleLoginResponse?, Error>) -> Void) {
106
+ if let user = GIDSignIn.sharedInstance.currentUser {
107
+ completion(.success(createLoginResponse(user: user)))
108
+ } else {
109
+ completion(.success(nil))
110
+ }
111
+ }
112
+
113
+ func refresh(completion: @escaping (Result<Void, Error>) -> Void) {
114
+ DispatchQueue.main.async {
115
+ guard let currentUser = GIDSignIn.sharedInstance.currentUser else {
116
+ completion(.failure(NSError(domain: "GoogleProvider", code: 0, userInfo: [NSLocalizedDescriptionKey: "User not logged in"])))
117
+ return
118
+ }
119
+ currentUser.refreshTokensIfNeeded { _, error in
120
+ if let error = error {
121
+ completion(.failure(error))
122
+ return
123
+ }
124
+ completion(.success(()))
125
+ }
126
+ }
127
+ }
128
+
129
+ private func getServerClientIdValue() -> String? {
130
+ // Implement your logic to retrieve the server client ID
131
+ return nil
132
+ }
133
+
134
+ private func createLoginResponse(user: GIDGoogleUser) -> GoogleLoginResponse {
135
+ return GoogleLoginResponse(
136
+ authentication: GoogleLoginResponse.Authentication(
137
+ accessToken: user.accessToken.tokenString,
138
+ idToken: user.idToken?.tokenString,
139
+ refreshToken: user.refreshToken.tokenString
140
+ ),
141
+ email: user.profile?.email,
142
+ familyName: user.profile?.familyName,
143
+ givenName: user.profile?.givenName,
144
+ id: user.userID,
145
+ name: user.profile?.name,
146
+ imageUrl: user.profile?.imageURL(withDimension: 100)?.absoluteString
147
+ )
148
+ }
149
+ }
150
+
151
+ struct GoogleLoginResponse {
152
+ let authentication: Authentication
153
+ let email: String?
154
+ let familyName: String?
155
+ let givenName: String?
156
+ let id: String?
157
+ let name: String?
158
+ let imageUrl: String?
159
+
160
+ struct Authentication {
161
+ let accessToken: String
162
+ let idToken: String?
163
+ let refreshToken: String?
164
+ }
165
+ }
@@ -0,0 +1,322 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ /**
5
+ * Please read the Capacitor iOS Plugin Development Guide
6
+ * here: https://capacitorjs.com/docs/plugins/ios
7
+ */
8
+ @objc(SocialLoginPlugin)
9
+ public class SocialLoginPlugin: CAPPlugin, CAPBridgedPlugin {
10
+ public let identifier = "SocialLoginPlugin"
11
+ public let jsName = "SocialLogin"
12
+ public let pluginMethods: [CAPPluginMethod] = [
13
+ CAPPluginMethod(name: "login", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "isLoggedIn", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "getAuthorizationCode", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "getUserInfo", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise)
19
+ ]
20
+ private let apple = AppleProvider()
21
+ private let facebook = FacebookProvider()
22
+ private let google = GoogleProvider()
23
+
24
+ @objc func initialize(_ call: CAPPluginCall) {
25
+ guard let options = call.options else {
26
+ call.reject("Missing options")
27
+ return
28
+ }
29
+
30
+ var initialized = false
31
+
32
+ if let facebookSettings = call.getObject("facebook") {
33
+ if let facebookAppId = facebookSettings["appId"] as? String {
34
+ facebook.initialize()
35
+ initialized = true
36
+ }
37
+ }
38
+
39
+ if let googleSettings = call.getObject("google") {
40
+ if let googleClientId = googleSettings["clientId"] as? String {
41
+ google.initialize(clientId: googleClientId)
42
+ initialized = true
43
+ }
44
+ }
45
+
46
+ if let appleSettings = call.getObject("apple") {
47
+ if let appleClientId = appleSettings["clientId"] as? String,
48
+ let redirectUrl = appleSettings["redirectUrl"] as? String {
49
+ apple.initialize(clientId: appleClientId, redirectUrl: redirectUrl)
50
+ initialized = true
51
+ }
52
+ }
53
+
54
+ if initialized {
55
+ call.resolve()
56
+ } else {
57
+ call.reject("No provider was initialized")
58
+ }
59
+ }
60
+
61
+ @objc func getAuthorizationCode(_ call: CAPPluginCall) {
62
+ guard let provider = call.getString("provider") else {
63
+ call.reject("Missing provider or options")
64
+ return
65
+ }
66
+
67
+ switch provider {
68
+ case "apple": do {
69
+ if let idToken = apple.idToken {
70
+ if !idToken.isEmpty {
71
+ call.resolve([ "jwt": idToken ])
72
+ } else {
73
+ call.reject("IdToken is empty")
74
+ }
75
+ } else {
76
+ call.reject("IdToken is nil")
77
+ }
78
+ }
79
+ case "google": do {
80
+ self.google.getAuthorizationCode { res in
81
+ do {
82
+ let authorizationCode = try res.get()
83
+ call.resolve([ "jwt": authorizationCode ])
84
+ } catch {
85
+ call.reject(error.localizedDescription)
86
+ }
87
+ }
88
+ }
89
+ default:
90
+ call.reject("Invalid provider")
91
+ }
92
+ }
93
+
94
+ @objc func isLoggedIn(_ call: CAPPluginCall) {
95
+ guard let provider = call.getString("provider") else {
96
+ call.reject("Missing provider or options")
97
+ return
98
+ }
99
+
100
+ switch provider {
101
+ case "apple": do {
102
+ if let idToken = apple.idToken {
103
+ if !idToken.isEmpty {
104
+ call.resolve([ "isLoggedIn": true ])
105
+ } else {
106
+ call.resolve([ "isLoggedIn": false ])
107
+ }
108
+ } else {
109
+ call.resolve([ "isLoggedIn": false ])
110
+ }
111
+ }
112
+ case "google": do {
113
+ self.google.isLoggedIn { res in
114
+ do {
115
+ let isLogged = try res.get()
116
+ call.resolve([ "isLoggedIn": isLogged ])
117
+ } catch {
118
+ call.reject(error.localizedDescription)
119
+ }
120
+ }
121
+ }
122
+ default:
123
+ call.reject("Invalid provider")
124
+ }
125
+ }
126
+
127
+ @objc func login(_ call: CAPPluginCall) {
128
+ guard let provider = call.getString("provider"),
129
+ let payload = call.getObject("options") else {
130
+ call.reject("Missing provider or options")
131
+ return
132
+ }
133
+
134
+ switch provider {
135
+ case "facebook":
136
+ facebook.login(payload: payload) { (result: Result<FacebookLoginResponse, Error>) in
137
+ self.handleLoginResult(result, call: call)
138
+ }
139
+ case "google":
140
+ google.login(payload: payload) { (result: Result<GoogleLoginResponse, Error>) in
141
+ self.handleLoginResult(result, call: call)
142
+ }
143
+ case "apple":
144
+ apple.login(payload: payload) { (result: Result<AppleProviderResponse, Error>) in
145
+ self.handleLoginResult(result, call: call)
146
+ }
147
+ default:
148
+ call.reject("Invalid provider")
149
+ }
150
+ }
151
+
152
+ @objc func logout(_ call: CAPPluginCall) {
153
+ guard let provider = call.getString("provider") else {
154
+ call.reject("Missing provider")
155
+ return
156
+ }
157
+
158
+ switch provider {
159
+ case "facebook":
160
+ facebook.logout { result in
161
+ self.handleLogoutResult(result, call: call)
162
+ }
163
+ case "google":
164
+ google.logout { result in
165
+ self.handleLogoutResult(result, call: call)
166
+ }
167
+ case "apple":
168
+ apple.logout { result in
169
+ self.handleLogoutResult(result, call: call)
170
+ }
171
+ default:
172
+ call.reject("Invalid provider")
173
+ }
174
+ }
175
+
176
+ @objc func getCurrentUser(_ call: CAPPluginCall) {
177
+ guard let provider = call.getString("provider") else {
178
+ call.reject("Missing provider")
179
+ return
180
+ }
181
+
182
+ switch provider {
183
+ case "facebook":
184
+ facebook.getCurrentUser { result in
185
+ self.handleCurrentUserResult(result, call: call)
186
+ }
187
+ case "google":
188
+ google.getCurrentUser { result in
189
+ self.handleCurrentUserResult(result, call: call)
190
+ }
191
+ case "apple":
192
+ apple.getCurrentUser { result in
193
+ self.handleCurrentUserResult(result, call: call)
194
+ }
195
+ default:
196
+ call.reject("Invalid provider")
197
+ }
198
+ }
199
+
200
+ @objc func refresh(_ call: CAPPluginCall) {
201
+ guard let provider = call.getString("provider") else {
202
+ call.reject("Missing provider")
203
+ return
204
+ }
205
+
206
+ switch provider {
207
+ case "facebook":
208
+ facebook.refresh(viewController: self.bridge?.viewController) { result in
209
+ self.handleRefreshResult(result, call: call)
210
+ }
211
+ case "google":
212
+ google.refresh { result in
213
+ self.handleRefreshResult(result, call: call)
214
+ }
215
+ case "apple":
216
+ apple.refresh { result in
217
+ self.handleRefreshResult(result, call: call)
218
+ }
219
+
220
+ default:
221
+ call.reject("Invalid provider")
222
+ }
223
+ }
224
+
225
+ private func handleLogoutResult<T>(_ result: Result<T, Error>, call: CAPPluginCall) {
226
+ switch result {
227
+ case .success:
228
+ call.resolve()
229
+ case .failure(let error):
230
+ call.reject(error.localizedDescription)
231
+ }
232
+ }
233
+
234
+ private func handleRefreshResult<T>(_ result: Result<T, Error>, call: CAPPluginCall) {
235
+ switch result {
236
+ case .success(let response):
237
+ if let user = response as? SocialLoginUser {
238
+ call.resolve([
239
+ "accessToken": user.accessToken,
240
+ "idToken": user.idToken,
241
+ "refreshToken": user.refreshToken,
242
+ "expiresIn": user.expiresIn
243
+ ])
244
+ } else {
245
+ call.reject("Invalid refresh response")
246
+ }
247
+ case .failure(let error):
248
+ call.reject(error.localizedDescription)
249
+ }
250
+ }
251
+
252
+ private func handleCurrentUserResult<T>(_ result: Result<T?, Error>, call: CAPPluginCall) {
253
+ switch result {
254
+ case .success(let response):
255
+ if let user = response as? SocialLoginUser {
256
+ call.resolve([
257
+ "accessToken": user.accessToken,
258
+ "idToken": user.idToken ?? "",
259
+ "refreshToken": user.refreshToken ?? "",
260
+ "expiresIn": user.expiresIn ?? 0
261
+ ])
262
+ } else {
263
+ call.reject("User not logged in")
264
+ }
265
+ case .failure(let error):
266
+ call.reject(error.localizedDescription)
267
+ }
268
+ }
269
+
270
+ private func handleLoginResult<T>(_ result: Result<T, Error>, call: CAPPluginCall) {
271
+ switch result {
272
+ case .success(let response):
273
+ if let appleResponse = response as? AppleProviderResponse {
274
+ call.resolve([
275
+ "provider": "apple",
276
+ "result": [
277
+ "identityToken": appleResponse.identityToken
278
+ ]
279
+ ])
280
+ } else if let googleResponse = response as? GoogleLoginResponse {
281
+ call.resolve([
282
+ "provider": "google",
283
+ "result": [
284
+ "accessToken": [
285
+ "token": googleResponse.authentication.accessToken,
286
+ "userId": googleResponse.id ?? ""
287
+ ],
288
+ "idToken": googleResponse.authentication.idToken ?? "",
289
+ // ""
290
+ "profile": [
291
+ "email": googleResponse.email ?? "",
292
+ "familyName": googleResponse.familyName ?? "",
293
+ "givenName": googleResponse.givenName ?? "",
294
+ "id": googleResponse.id ?? "",
295
+ "name": googleResponse.name ?? "",
296
+ "imageUrl": googleResponse.imageUrl ?? ""
297
+ ]
298
+ ]
299
+ ])
300
+ } else if let facebookResponse = response as? FacebookLoginResponse {
301
+ call.resolve([
302
+ "provider": "facebook",
303
+ "result": [
304
+ "accessToken": facebookResponse.accessToken,
305
+ "profile": facebookResponse.profile
306
+ ]
307
+ ])
308
+ } else {
309
+ call.reject("Unsupported provider response")
310
+ }
311
+ case .failure(let error):
312
+ call.reject(error.localizedDescription)
313
+ }
314
+ }
315
+ }
316
+
317
+ struct SocialLoginUser {
318
+ let accessToken: String
319
+ let idToken: String?
320
+ let refreshToken: String?
321
+ let expiresIn: Int?
322
+ }
@@ -0,0 +1,15 @@
1
+ import XCTest
2
+ @testable import SocialLoginPlugin
3
+
4
+ class SocialLoginTests: XCTestCase {
5
+ func testEcho() {
6
+ // This is an example of a functional test case for a plugin.
7
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
8
+
9
+ let implementation = SocialLogin()
10
+ let value = "Hello, World!"
11
+ let result = implementation.echo(value)
12
+
13
+ XCTAssertEqual(value, result)
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@capgo/capacitor-social-login",
3
+ "version": "0.0.10",
4
+ "description": "All social logins in one plugin",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapgoCapacitorSocialLogin.podspec"
17
+ ],
18
+ "author": "Martin Donadieu <martin@capgo.app>",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Cap-go/capacitor-social-login.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Cap-go/capacitor-social-login/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "native",
31
+ "social",
32
+ "login"
33
+ ],
34
+ "scripts": {
35
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
36
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorSocialLogin -destination generic/platform=iOS",
37
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
38
+ "verify:web": "npm run build",
39
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
40
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --autocorrect --format",
41
+ "eslint": "eslint . --ext .ts",
42
+ "prettier": "prettier --config .prettierrc.js \"**/*.{css,html,ts,js,java}\"",
43
+ "swiftlint": "node-swiftlint",
44
+ "docgen": "docgen --api SocialLoginPlugin --output-readme README.md --output-json dist/docs.json",
45
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
46
+ "clean": "rimraf ./dist",
47
+ "watch": "tsc --watch",
48
+ "prepublishOnly": "npm run build"
49
+ },
50
+ "devDependencies": {
51
+ "@capacitor/android": "^6.0.0",
52
+ "@capacitor/cli": "^6.0.0",
53
+ "@capacitor/core": "^6.0.0",
54
+ "@capacitor/docgen": "^0.2.2",
55
+ "@capacitor/ios": "^6.0.0",
56
+ "@ionic/eslint-config": "^0.4.0",
57
+ "@ionic/prettier-config": "^4.0.0",
58
+ "@ionic/swiftlint-config": "^2.0.0",
59
+ "@types/node": "^20.12.12",
60
+ "@typescript-eslint/eslint-plugin": "^8.7.0",
61
+ "@typescript-eslint/parser": "^8.7.0",
62
+ "eslint": "^8.57.1",
63
+ "eslint-plugin-import": "^2.30.0",
64
+ "prettier": "^3.3.3",
65
+ "prettier-plugin-java": "^2.6.4",
66
+ "rimraf": "^6.0.1",
67
+ "rollup": "^4.22.4",
68
+ "swiftlint": "^2.0.0",
69
+ "typescript": "^5.4.5"
70
+ },
71
+ "peerDependencies": {
72
+ "@capacitor/core": "^6.0.0"
73
+ },
74
+ "prettier": "@ionic/prettier-config",
75
+ "eslintConfig": {
76
+ "extends": "@ionic/eslint-config/recommended"
77
+ },
78
+ "capacitor": {
79
+ "ios": {
80
+ "src": "ios"
81
+ },
82
+ "android": {
83
+ "src": "android"
84
+ }
85
+ },
86
+ "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1"
87
+ }