react-native-oauth-essentials 1.0.11 → 1.1.1

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 (51) hide show
  1. package/README.md +384 -362
  2. package/android/src/main/java/com/oauthessentials/CredentialError.kt +4 -1
  3. package/android/src/main/java/com/oauthessentials/CredentialFactory.kt +24 -2
  4. package/android/src/main/java/com/oauthessentials/CustomTabsActivity.kt +14 -1
  5. package/android/src/main/java/com/oauthessentials/OauthEssentialsModule.kt +448 -192
  6. package/app.plugin.js +1 -0
  7. package/ios/AppleIdRetriever.swift +25 -26
  8. package/ios/CredentialError.swift +3 -0
  9. package/ios/CredentialFactory.swift +38 -20
  10. package/ios/CredentialsType.swift +1 -0
  11. package/ios/GoogleIdRetriever.swift +132 -56
  12. package/ios/HybridRetriever.swift +12 -18
  13. package/ios/OauthEssentials.h +0 -3
  14. package/ios/OauthEssentials.mm +45 -2
  15. package/ios/OauthEssentialsModule.swift +266 -57
  16. package/ios/PasskeyRetriever.swift +31 -49
  17. package/ios/PasswordRetriever.swift +10 -30
  18. package/ios/RCTPromiseSettler.swift +125 -0
  19. package/jest/index.js +149 -0
  20. package/lib/module/NativeOauthEssentials.js +10 -1
  21. package/lib/module/NativeOauthEssentials.js.map +1 -1
  22. package/lib/module/errors.js +38 -0
  23. package/lib/module/errors.js.map +1 -0
  24. package/lib/module/index.js +432 -58
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/oauth.js +97 -0
  27. package/lib/module/oauth.js.map +1 -0
  28. package/lib/module/platform.js +108 -0
  29. package/lib/module/platform.js.map +1 -0
  30. package/lib/module/types.js +46 -1
  31. package/lib/module/types.js.map +1 -1
  32. package/lib/typescript/src/NativeOauthEssentials.d.ts +70 -16
  33. package/lib/typescript/src/NativeOauthEssentials.d.ts.map +1 -1
  34. package/lib/typescript/src/errors.d.ts +18 -0
  35. package/lib/typescript/src/errors.d.ts.map +1 -0
  36. package/lib/typescript/src/index.d.ts +235 -40
  37. package/lib/typescript/src/index.d.ts.map +1 -1
  38. package/lib/typescript/src/oauth.d.ts +32 -0
  39. package/lib/typescript/src/oauth.d.ts.map +1 -0
  40. package/lib/typescript/src/platform.d.ts +39 -0
  41. package/lib/typescript/src/platform.d.ts.map +1 -0
  42. package/lib/typescript/src/types.d.ts +190 -3
  43. package/lib/typescript/src/types.d.ts.map +1 -1
  44. package/package.json +8 -4
  45. package/plugin/withOauthEssentials.js +291 -0
  46. package/src/NativeOauthEssentials.ts +91 -17
  47. package/src/errors.ts +49 -0
  48. package/src/index.tsx +587 -80
  49. package/src/oauth.ts +140 -0
  50. package/src/platform.ts +121 -0
  51. package/src/types.ts +200 -2
@@ -2,35 +2,57 @@ import Foundation
2
2
  import React
3
3
  import GoogleSignIn
4
4
  import AuthenticationServices
5
+ import CryptoKit
5
6
  import Security
6
7
  import UIKit
7
8
 
9
+ /// TurboModule methods are called on the module queue. Every method hops to the main thread, so
10
+ /// the retrievers, the simultaneous call lock and all UIKit/AuthenticationServices calls run on main.
8
11
  public class OauthEssentialsModule: NSObject {
9
12
 
10
13
  public var onCredentialSuccess: ((NSDictionary) -> Void)?
11
14
  public var onCredentialFailure: ((NSDictionary) -> Void)?
15
+ public var onAppleIdCredentialRevoked: ((NSDictionary) -> Void)?
12
16
 
13
17
  private var _password_retriever: PasswordRetriever? = nil
14
18
  private var _google_id_retriever: GoogleIdRetriever? = nil
15
19
  private var _apple_id_retriever: AppleIdRetriever? = nil
16
20
  private var _hybrid_retriever: HybridRetriever? = nil
17
21
  private var _passkey_retriever: RCTPromiseSettler? = nil
22
+ private var _auth_session_retriever: AuthSessionRetriever? = nil
23
+ private var revokedObserver: NSObjectProtocol?
18
24
 
19
25
  @objc
20
26
  public init(
21
27
  onCredentialSuccess: @escaping ((NSDictionary) -> Void),
22
- onCredentialFailure: @escaping ((NSDictionary) -> Void)
28
+ onCredentialFailure: @escaping ((NSDictionary) -> Void),
29
+ onAppleIdCredentialRevoked: @escaping ((NSDictionary) -> Void)
23
30
  ) {
24
31
  super.init()
25
32
 
26
33
  self.onCredentialSuccess = onCredentialSuccess
27
34
  self.onCredentialFailure = onCredentialFailure
35
+ self.onAppleIdCredentialRevoked = onAppleIdCredentialRevoked
36
+
37
+ self.revokedObserver = NotificationCenter.default.addObserver(
38
+ forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
39
+ object: nil,
40
+ queue: .main
41
+ ) { [weak self] _ in
42
+ self?.onAppleIdCredentialRevoked?(["type": "REVOKED"])
43
+ }
28
44
  }
29
45
 
30
- private func ensurePreviousPromiseSettled(settler: RCTPromiseSettler?, reject: RCTPromiseRejectBlock) -> Bool {
31
- if settler == nil || (settler != nil && ((settler?.settled()) != nil)) {
32
- return true
46
+ deinit {
47
+ if let revokedObserver = revokedObserver {
48
+ NotificationCenter.default.removeObserver(revokedObserver)
33
49
  }
50
+ }
51
+
52
+ /// Returns true when the previous call of the same method already settled its promise.
53
+ private func ensurePreviousPromiseSettled(settler: RCTPromiseSettler?, reject: RCTPromiseRejectBlock) -> Bool {
54
+ guard let settler = settler else { return true }
55
+ if settler.settled() { return true }
34
56
 
35
57
  reject(
36
58
  CredentialError.simultaneousCallError.rawValue,
@@ -65,7 +87,8 @@ public class OauthEssentialsModule: NSObject {
65
87
  "GOOGLE_PLAY_SERVICES_SUPPORTED": false, // Not applicable on iOS, it does not have Google Play Services
66
88
  "APPLE_ID_SUPPORTED": appleIdSupported,
67
89
  "HYBRID_SUPPORTED": passwordSupported || appleIdSupported,
68
- "PASSKEYS_SUPPORTED": passkeySupported
90
+ "PASSKEYS_SUPPORTED": passkeySupported,
91
+ "OAUTH_REDIRECT_URI": "\((Bundle.main.bundleIdentifier ?? "app").lowercased())://oauth/callback"
69
92
  ]
70
93
  }
71
94
 
@@ -76,11 +99,40 @@ public class OauthEssentialsModule: NSObject {
76
99
  resolver resolve: @escaping RCTPromiseResolveBlock,
77
100
  rejecter reject: @escaping RCTPromiseRejectBlock
78
101
  ) {
79
- if (ensurePreviousPromiseSettled(settler: self._google_id_retriever,reject: reject)) {
80
- self._google_id_retriever = GoogleIdRetriever(clientId: clientId, options: options,resolve: resolve,reject: reject, emitEvent: emitEvent)
102
+ DispatchQueue.main.async {
103
+ if self.ensurePreviousPromiseSettled(settler: self._google_id_retriever, reject: reject) {
104
+ self._google_id_retriever = GoogleIdRetriever(clientId: clientId, options: options, silent: false, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
105
+ }
106
+ }
107
+ }
108
+
109
+ @objc
110
+ public func googleSilentSignIn(
111
+ _ clientId: String,
112
+ options: NSDictionary,
113
+ resolver resolve: @escaping RCTPromiseResolveBlock,
114
+ rejecter reject: @escaping RCTPromiseRejectBlock
115
+ ) {
116
+ DispatchQueue.main.async {
117
+ if self.ensurePreviousPromiseSettled(settler: self._google_id_retriever, reject: reject) {
118
+ self._google_id_retriever = GoogleIdRetriever(clientId: clientId, options: options, silent: true, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
119
+ }
81
120
  }
82
121
  }
83
122
 
123
+ @objc
124
+ public func signOut(
125
+ _ resolve: @escaping RCTPromiseResolveBlock,
126
+ rejecter reject: @escaping RCTPromiseRejectBlock
127
+ ) {
128
+ DispatchQueue.main.async {
129
+ GIDSignIn.sharedInstance.signOut()
130
+ resolve(true)
131
+ }
132
+ }
133
+
134
+ /// iOS has no API to save a password without an associated domain: let the system offer it
135
+ /// through the text fields `textContentType` instead.
84
136
  @objc
85
137
  public func passwordSignIn(
86
138
  _ username: String,
@@ -88,9 +140,7 @@ public class OauthEssentialsModule: NSObject {
88
140
  resolver resolve: @escaping RCTPromiseResolveBlock,
89
141
  rejecter reject: @escaping RCTPromiseRejectBlock
90
142
  ) {
91
- if (ensurePreviousPromiseSettled(settler: self._apple_id_retriever, reject: reject)) {
92
- self._apple_id_retriever = AppleIdRetriever(resolve: resolve, reject: reject, emitEvent: self.emitEvent)
93
- }
143
+ resolve(false)
94
144
  }
95
145
 
96
146
  @objc
@@ -98,9 +148,11 @@ public class OauthEssentialsModule: NSObject {
98
148
  _ resolve: @escaping RCTPromiseResolveBlock,
99
149
  rejecter reject: @escaping RCTPromiseRejectBlock
100
150
  ) {
101
- if (ensurePreviousPromiseSettled(settler: self._password_retriever,reject: reject)) {
102
- self._password_retriever = PasswordRetriever(resolve: resolve,reject: reject, emitEvent: self.emitEvent)
151
+ DispatchQueue.main.async {
152
+ if self.ensurePreviousPromiseSettled(settler: self._password_retriever, reject: reject) {
153
+ self._password_retriever = PasswordRetriever(resolve: resolve, reject: reject, emitEvent: self.emitEvent)
103
154
  }
155
+ }
104
156
  }
105
157
 
106
158
  @objc
@@ -109,8 +161,10 @@ public class OauthEssentialsModule: NSObject {
109
161
  options: NSDictionary,
110
162
  resolver resolve: @escaping RCTPromiseResolveBlock,
111
163
  rejecter reject: @escaping RCTPromiseRejectBlock) {
112
- if (ensurePreviousPromiseSettled(settler: self._hybrid_retriever,reject: reject)) {
113
- self._hybrid_retriever = HybridRetriever(clientId: clientId, options: options, resolve: resolve, reject: reject, emitEvent: emitEvent)
164
+ DispatchQueue.main.async {
165
+ if self.ensurePreviousPromiseSettled(settler: self._hybrid_retriever, reject: reject) {
166
+ self._hybrid_retriever = HybridRetriever(clientId: clientId, options: options, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
167
+ }
114
168
  }
115
169
  }
116
170
 
@@ -119,6 +173,24 @@ public class OauthEssentialsModule: NSObject {
119
173
  _ options: NSDictionary,
120
174
  resolver resolve: @escaping RCTPromiseResolveBlock,
121
175
  rejecter reject: @escaping RCTPromiseRejectBlock
176
+ ) {
177
+ performPassKey(options: options, registration: true, resolve: resolve, reject: reject)
178
+ }
179
+
180
+ @objc(getPassKey:resolver:rejecter:)
181
+ public func getPassKey(
182
+ _ options: NSDictionary,
183
+ resolver resolve: @escaping RCTPromiseResolveBlock,
184
+ rejecter reject: @escaping RCTPromiseRejectBlock
185
+ ) {
186
+ performPassKey(options: options, registration: false, resolve: resolve, reject: reject)
187
+ }
188
+
189
+ private func performPassKey(
190
+ options: NSDictionary,
191
+ registration: Bool,
192
+ resolve: @escaping RCTPromiseResolveBlock,
193
+ reject: @escaping RCTPromiseRejectBlock
122
194
  ) {
123
195
  guard #available(iOS 16.0, *) else {
124
196
  reject(
@@ -129,69 +201,122 @@ public class OauthEssentialsModule: NSObject {
129
201
  return
130
202
  }
131
203
 
132
- if ensurePreviousPromiseSettled(settler: self._passkey_retriever, reject: reject) {
133
- guard let requestJson = options["requestJson"] as? String else {
134
- reject(
135
- CredentialError.invalidResult.rawValue,
136
- "\"requestJson\" field is required",
137
- nil
138
- )
139
- return
204
+ DispatchQueue.main.async {
205
+ // A pending AutoFill request must not block an explicit passkey request.
206
+ if let pending = self._passkey_retriever as? PasskeyRetriever, pending.autoFill, !pending.settled() {
207
+ pending.cancel()
208
+ pending.succeed(CredentialFactory.fromCancelled())
140
209
  }
141
210
 
142
- self._passkey_retriever = PasskeyRetriever(
143
- requestJson: requestJson,
144
- mode: .registration,
145
- resolve: resolve,
146
- reject: reject,
147
- emitEvent: emitEvent
148
- )
211
+ if self.ensurePreviousPromiseSettled(settler: self._passkey_retriever, reject: reject) {
212
+ guard let requestJson = options["requestJson"] as? String else {
213
+ reject(
214
+ CredentialError.invalidResult.rawValue,
215
+ "\"requestJson\" field is required",
216
+ nil
217
+ )
218
+ return
219
+ }
220
+
221
+ self._passkey_retriever = PasskeyRetriever(
222
+ requestJson: requestJson,
223
+ mode: registration ? .registration : .assertion,
224
+ autoFill: (options["mediation"] as? String) == "autofill",
225
+ resolve: resolve,
226
+ reject: reject,
227
+ emitEvent: self.emitEvent
228
+ )
229
+ }
149
230
  }
150
231
  }
151
232
 
152
- @objc(getPassKey:resolver:rejecter:)
153
- public func getPassKey(
154
- _ options: NSDictionary,
155
- resolver resolve: @escaping RCTPromiseResolveBlock,
233
+ @objc
234
+ public func cancelPassKeyAutoFill(
235
+ _ resolve: @escaping RCTPromiseResolveBlock,
156
236
  rejecter reject: @escaping RCTPromiseRejectBlock
157
237
  ) {
158
238
  guard #available(iOS 16.0, *) else {
159
- reject(
160
- CredentialError.notSupportedError.rawValue,
161
- "Passkeys require iOS 16.0 or later",
162
- nil
163
- )
239
+ resolve(false)
164
240
  return
165
241
  }
166
242
 
167
- if ensurePreviousPromiseSettled(settler: self._passkey_retriever, reject: reject) {
168
- guard let requestJson = options["requestJson"] as? String else {
169
- reject(
170
- CredentialError.invalidResult.rawValue,
171
- "\"requestJson\" field is required",
172
- nil
173
- )
243
+ DispatchQueue.main.async {
244
+ guard let pending = self._passkey_retriever as? PasskeyRetriever, pending.autoFill, !pending.settled() else {
245
+ resolve(false)
174
246
  return
175
247
  }
176
-
177
- self._passkey_retriever = PasskeyRetriever(
178
- requestJson: requestJson,
179
- mode: .assertion,
180
- resolve: resolve,
181
- reject: reject,
182
- emitEvent: emitEvent
183
- )
248
+ pending.cancel()
249
+ pending.succeed(CredentialFactory.fromCancelled())
250
+ resolve(true)
184
251
  }
185
252
  }
186
253
 
187
- @objc(appleSignIn:resolver:rejecter:)
254
+ @objc(appleSignIn:options:resolver:rejecter:)
188
255
  public func appleSignIn(
189
- _ webAndroidUrl: String,
256
+ _ webAndroidUrl: String?,
257
+ options: NSDictionary,
190
258
  resolver resolve: @escaping RCTPromiseResolveBlock,
191
259
  rejecter reject: @escaping RCTPromiseRejectBlock
192
260
  ) {
193
- if (ensurePreviousPromiseSettled(settler: self._apple_id_retriever,reject: reject)) {
194
- self._apple_id_retriever = AppleIdRetriever(resolve: resolve,reject: reject, emitEvent: emitEvent)
261
+ DispatchQueue.main.async {
262
+ if self.ensurePreviousPromiseSettled(settler: self._apple_id_retriever, reject: reject) {
263
+ self._apple_id_retriever = AppleIdRetriever(options: options, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
264
+ }
265
+ }
266
+ }
267
+
268
+ @objc(getAppleIdCredentialState:resolver:rejecter:)
269
+ public func getAppleIdCredentialState(
270
+ _ userId: String,
271
+ resolver resolve: @escaping RCTPromiseResolveBlock,
272
+ rejecter reject: @escaping RCTPromiseRejectBlock
273
+ ) {
274
+ ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userId) { state, error in
275
+ if let error = error {
276
+ reject(CredentialError.invalidResult.rawValue, error.localizedDescription, error)
277
+ return
278
+ }
279
+ switch state {
280
+ case .authorized:
281
+ resolve("AUTHORIZED")
282
+ case .revoked:
283
+ resolve("REVOKED")
284
+ case .transferred:
285
+ resolve("TRANSFERRED")
286
+ default:
287
+ resolve("NOT_FOUND")
288
+ }
289
+ }
290
+ }
291
+
292
+ @objc
293
+ public func generatePkce(
294
+ _ resolve: @escaping RCTPromiseResolveBlock,
295
+ rejecter reject: @escaping RCTPromiseRejectBlock
296
+ ) {
297
+ guard let codeVerifier = Pkce.randomString(byteCount: 32),
298
+ let state = Pkce.randomString(byteCount: 16) else {
299
+ reject(CredentialError.invalidResult.rawValue, "Unable to generate random bytes", nil)
300
+ return
301
+ }
302
+ resolve([
303
+ "codeVerifier": codeVerifier,
304
+ "codeChallenge": Pkce.challenge(for: codeVerifier),
305
+ "state": state
306
+ ])
307
+ }
308
+
309
+ @objc(openAuthSession:options:resolver:rejecter:)
310
+ public func openAuthSession(
311
+ _ url: String,
312
+ options: NSDictionary,
313
+ resolver resolve: @escaping RCTPromiseResolveBlock,
314
+ rejecter reject: @escaping RCTPromiseRejectBlock
315
+ ) {
316
+ DispatchQueue.main.async {
317
+ if self.ensurePreviousPromiseSettled(settler: self._auth_session_retriever, reject: reject) {
318
+ self._auth_session_retriever = AuthSessionRetriever(url: url, options: options, resolve: resolve, reject: reject)
319
+ }
195
320
  }
196
321
  }
197
322
 
@@ -208,3 +333,87 @@ public class OauthEssentialsModule: NSObject {
208
333
  }
209
334
  }
210
335
  }
336
+
337
+ enum Pkce {
338
+ static func randomString(byteCount: Int) -> String? {
339
+ var bytes = [UInt8](repeating: 0, count: byteCount)
340
+ guard SecRandomCopyBytes(kSecRandomDefault, byteCount, &bytes) == errSecSuccess else {
341
+ return nil
342
+ }
343
+ return Data(bytes).base64URLEncodedString()
344
+ }
345
+
346
+ static func challenge(for verifier: String) -> String {
347
+ return Data(SHA256.hash(data: Data(verifier.utf8))).base64URLEncodedString()
348
+ }
349
+ }
350
+
351
+ /// Opens an url in ASWebAuthenticationSession and resolves with the redirect url.
352
+ /// It does not emit events: oauthSignIn finishes the flow in JS.
353
+ public class AuthSessionRetriever: RCTPromiseSettler, ASWebAuthenticationPresentationContextProviding {
354
+
355
+ private var session: ASWebAuthenticationSession?
356
+
357
+ init(
358
+ url: String,
359
+ options: NSDictionary,
360
+ resolve: @escaping RCTPromiseResolveBlock,
361
+ reject: @escaping RCTPromiseRejectBlock
362
+ ) {
363
+ super.init(resolve: resolve, reject: reject, emitEvent: nil)
364
+
365
+ guard let authUrl = URL(string: url),
366
+ let callbackScheme = options["callbackScheme"] as? String else {
367
+ fail(.invalidResult, "Invalid authorization url or redirect uri")
368
+ return
369
+ }
370
+ whenBrowserCanOpen("oauthSignIn") { _ in
371
+ let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { callbackURL, error in
372
+ self.session = nil
373
+ self.handle(callbackURL: callbackURL, error: error)
374
+ }
375
+ session.presentationContextProvider = self
376
+ session.prefersEphemeralWebBrowserSession = (options["prefersEphemeralSession"] as? Bool) ?? false
377
+ self.session = session
378
+
379
+ if !session.start() {
380
+ self.session = nil
381
+ self.fail(.browserUnavailable, "Unable to start the browser session")
382
+ }
383
+ }
384
+ }
385
+
386
+ private func handle(callbackURL: URL?, error: Error?) {
387
+ if let error = error {
388
+ let nsError = error as NSError
389
+ if nsError.domain == ASWebAuthenticationSessionError.errorDomain {
390
+ switch nsError.code {
391
+ case ASWebAuthenticationSessionError.canceledLogin.rawValue:
392
+ succeed(CredentialFactory.fromCancelled())
393
+ return
394
+ case ASWebAuthenticationSessionError.presentationContextNotProvided.rawValue,
395
+ ASWebAuthenticationSessionError.presentationContextInvalid.rawValue:
396
+ fail(.browserUnavailable, error.localizedDescription, error)
397
+ return
398
+ default:
399
+ break
400
+ }
401
+ }
402
+ fail(.invalidResult, error.localizedDescription, error)
403
+ return
404
+ }
405
+
406
+ guard let callbackURL = callbackURL else {
407
+ fail(.invalidResult, "No redirect url returned")
408
+ return
409
+ }
410
+ succeed([
411
+ "type": CredentialsType.redirect.rawValue,
412
+ "data": ["url": callbackURL.absoluteString]
413
+ ])
414
+ }
415
+
416
+ public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
417
+ return PresentationContext.anchor()
418
+ }
419
+ }
@@ -20,15 +20,20 @@ public class PasskeyRetriever: RCTPromiseSettler,
20
20
  }
21
21
 
22
22
  private let mode: Mode
23
+ /// AutoFill requests wait for the user to pick a passkey in the QuickType bar.
24
+ let autoFill: Bool
25
+ private var controller: ASAuthorizationController?
23
26
 
24
27
  init(
25
28
  requestJson: String,
26
29
  mode: Mode,
30
+ autoFill: Bool,
27
31
  resolve: @escaping RCTPromiseResolveBlock,
28
32
  reject: @escaping RCTPromiseRejectBlock,
29
33
  emitEvent: ((String, NSDictionary) -> Void)?
30
34
  ) {
31
35
  self.mode = mode
36
+ self.autoFill = autoFill && mode == .assertion
32
37
  super.init(resolve: resolve, reject: reject, emitEvent: emitEvent)
33
38
 
34
39
  do {
@@ -36,17 +41,22 @@ public class PasskeyRetriever: RCTPromiseSettler,
36
41
  let controller = ASAuthorizationController(authorizationRequests: [request])
37
42
  controller.delegate = self
38
43
  controller.presentationContextProvider = self
39
- controller.performRequests(options: .preferImmediatelyAvailableCredentials)
44
+ self.controller = controller
45
+ if self.autoFill {
46
+ controller.performAutoFillAssistedRequests()
47
+ } else {
48
+ controller.performRequests(options: .preferImmediatelyAvailableCredentials)
49
+ }
40
50
  } catch {
41
- reject(
42
- CredentialError.invalidResult.rawValue,
43
- "Failed to parse requestJson: \(error.localizedDescription)",
44
- error
45
- )
46
- cleanup()
51
+ fail(.invalidResult, "Failed to parse requestJson: \(error.localizedDescription)", error)
47
52
  }
48
53
  }
49
54
 
55
+ /// Cancels the pending request, which resolves it as cancelled.
56
+ func cancel() {
57
+ controller?.cancel()
58
+ }
59
+
50
60
  private func buildRequest(requestJson: String, mode: Mode) throws -> ASAuthorizationRequest {
51
61
  guard let data = requestJson.data(using: .utf8),
52
62
  let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
@@ -65,7 +75,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
65
75
 
66
76
  private func buildRegistrationRequest(json: [String: Any]) throws -> ASAuthorizationRequest {
67
77
  guard let challengeStr = json["challenge"] as? String,
68
- let challenge = base64URLDecode(challengeStr) else {
78
+ let challenge = Data(base64URLEncoded: challengeStr) else {
69
79
  throw NSError(domain: "PasskeyRetriever", code: -1, userInfo: [
70
80
  NSLocalizedDescriptionKey: "Missing or invalid challenge"
71
81
  ])
@@ -80,7 +90,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
80
90
 
81
91
  guard let user = json["user"] as? [String: Any],
82
92
  let userIdStr = user["id"] as? String,
83
- let userId = base64URLDecode(userIdStr),
93
+ let userId = Data(base64URLEncoded: userIdStr),
84
94
  let userName = user["name"] as? String else {
85
95
  throw NSError(domain: "PasskeyRetriever", code: -1, userInfo: [
86
96
  NSLocalizedDescriptionKey: "Missing or invalid user fields"
@@ -107,7 +117,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
107
117
  if #available(iOS 17.4, *) {
108
118
  request.excludedCredentials = excludeCredentials.compactMap { cred in
109
119
  guard let idStr = cred["id"] as? String,
110
- let idData = base64URLDecode(idStr) else { return nil }
120
+ let idData = Data(base64URLEncoded: idStr) else { return nil }
111
121
  return ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: idData)
112
122
  }
113
123
  } else {
@@ -120,7 +130,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
120
130
 
121
131
  private func buildAssertionRequest(json: [String: Any]) throws -> ASAuthorizationRequest {
122
132
  guard let challengeStr = json["challenge"] as? String,
123
- let challenge = base64URLDecode(challengeStr) else {
133
+ let challenge = Data(base64URLEncoded: challengeStr) else {
124
134
  throw NSError(domain: "PasskeyRetriever", code: -1, userInfo: [
125
135
  NSLocalizedDescriptionKey: "Missing or invalid challenge"
126
136
  ])
@@ -134,7 +144,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
134
144
  if let allowCredentials = json["allowCredentials"] as? [[String: Any]] {
135
145
  request.allowedCredentials = allowCredentials.compactMap { cred in
136
146
  guard let idStr = cred["id"] as? String,
137
- let idData = base64URLDecode(idStr) else { return nil }
147
+ let idData = Data(base64URLEncoded: idStr) else { return nil }
138
148
  return ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: idData)
139
149
  }
140
150
  }
@@ -152,66 +162,38 @@ public class PasskeyRetriever: RCTPromiseSettler,
152
162
  controller: ASAuthorizationController,
153
163
  didCompleteWithAuthorization authorization: ASAuthorization
154
164
  ) {
165
+ self.controller = nil
155
166
  switch mode {
156
167
  case .registration:
157
168
  if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
158
- let result = CredentialFactory.fromPasskeyRegistration(credential: credential)
159
- self.resolve?(result)
160
- self.emitEvent?("onCredentialSuccess", result as NSDictionary)
169
+ succeed(CredentialFactory.fromPasskeyRegistration(credential: credential))
170
+ return
161
171
  }
162
172
  case .assertion:
163
173
  if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
164
- let result = CredentialFactory.fromPasskeyAssertion(credential: credential)
165
- self.resolve?(result)
166
- self.emitEvent?("onCredentialSuccess", result as NSDictionary)
174
+ succeed(CredentialFactory.fromPasskeyAssertion(credential: credential))
175
+ return
167
176
  }
168
177
  }
169
- cleanup()
178
+ fail(.invalidResult, "Unexpected credential type")
170
179
  }
171
180
 
172
181
  public func authorizationController(
173
182
  controller: ASAuthorizationController,
174
183
  didCompleteWithError error: Error
175
184
  ) {
176
- let nsError = error as NSError
177
- if nsError.domain == ASAuthorizationError.errorDomain,
178
- nsError.code == ASAuthorizationError.canceled.rawValue {
179
- let result = CredentialFactory.fromCancelled()
180
- self.resolve?(result)
181
- self.emitEvent?("onCredentialSuccess", result as NSDictionary)
182
- } else {
183
- self.reject?(
184
- CredentialError.invalidResult.rawValue,
185
- error.localizedDescription,
186
- error
187
- )
188
- self.emitEvent?("onCredentialFailure", [
189
- "code": CredentialError.invalidResult.rawValue,
190
- "message": error.localizedDescription
191
- ])
192
- }
193
- cleanup()
185
+ self.controller = nil
186
+ finish(authorizationError: error)
194
187
  }
195
188
 
196
189
  // MARK: - ASAuthorizationControllerPresentationContextProviding
197
190
 
198
191
  public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
199
- return UIWindow()
192
+ return PresentationContext.anchor()
200
193
  }
201
194
 
202
195
  // MARK: - Helpers
203
196
 
204
- private func base64URLDecode(_ string: String) -> Data? {
205
- var base64 = string
206
- .replacingOccurrences(of: "-", with: "+")
207
- .replacingOccurrences(of: "_", with: "/")
208
- let remainder = base64.count % 4
209
- if remainder > 0 {
210
- base64.append(String(repeating: "=", count: 4 - remainder))
211
- }
212
- return Data(base64Encoded: base64)
213
- }
214
-
215
197
  private func mapUserVerification(_ value: String) -> ASAuthorizationPublicKeyCredentialUserVerificationPreference {
216
198
  switch value {
217
199
  case "required":
@@ -5,6 +5,8 @@ public class PasswordRetriever: RCTPromiseSettler,
5
5
  ASAuthorizationControllerDelegate,
6
6
  ASAuthorizationControllerPresentationContextProviding {
7
7
 
8
+ private var controller: ASAuthorizationController?
9
+
8
10
  override init(
9
11
  resolve: @escaping RCTPromiseResolveBlock,
10
12
  reject: @escaping RCTPromiseRejectBlock,
@@ -16,6 +18,7 @@ public class PasswordRetriever: RCTPromiseSettler,
16
18
  let controller = ASAuthorizationController(authorizationRequests: [request])
17
19
  controller.delegate = self
18
20
  controller.presentationContextProvider = self
21
+ self.controller = controller
19
22
  if #available(iOS 16.0, *) {
20
23
  controller.performRequests(options: .preferImmediatelyAvailableCredentials)
21
24
  } else {
@@ -27,12 +30,11 @@ public class PasswordRetriever: RCTPromiseSettler,
27
30
  controller: ASAuthorizationController,
28
31
  didCompleteWithAuthorization authorization: ASAuthorization
29
32
  ) {
33
+ self.controller = nil
30
34
  if let credential = authorization.credential as? ASPasswordCredential {
31
- let result = CredentialFactory.fromPassword(credential: credential)
32
- self.resolve?(result)
33
- self.emitEvent?("onCredentialSuccess", result as NSDictionary)
34
-
35
- cleanup()
35
+ succeed(CredentialFactory.fromPassword(credential: credential))
36
+ } else {
37
+ fail(.invalidResult, "Unexpected credential type")
36
38
  }
37
39
  }
38
40
 
@@ -40,33 +42,11 @@ public class PasswordRetriever: RCTPromiseSettler,
40
42
  controller: ASAuthorizationController,
41
43
  didCompleteWithError error: Error
42
44
  ) {
43
- let nsError = error as NSError
44
- if nsError.domain == ASAuthorizationError.errorDomain,
45
- nsError.code == ASAuthorizationError.canceled.rawValue {
46
-
47
- let result = CredentialFactory.fromCancelled()
48
- self.resolve?(result)
49
- self.emitEvent?("onCredentialSuccess", result as NSDictionary)
50
- } else {
51
- self.reject?(
52
- CredentialError.invalidResult.rawValue,
53
- error.localizedDescription,
54
- error
55
- )
56
- self.emitEvent?("onCredentialFailure", [
57
- "code": CredentialError.invalidResult.rawValue,
58
- "message": error.localizedDescription
59
- ])
60
- }
61
-
62
- self.cleanup()
45
+ self.controller = nil
46
+ finish(authorizationError: error)
63
47
  }
64
48
 
65
49
  public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
66
- return UIWindow()
67
- }
68
-
69
- func getIsSettled() -> Bool {
70
- return resolve == nil || reject == nil
50
+ return PresentationContext.anchor()
71
51
  }
72
52
  }