@capgo/capacitor-passkey 8.3.10 → 8.5.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.
@@ -1,3 +1,5 @@
1
+ import AuthenticationServices
2
+ import Capacitor
1
3
  import Foundation
2
4
 
3
5
  @objc public class CapacitorPasskey: NSObject {
@@ -31,6 +33,36 @@ import Foundation
31
33
 
32
34
  return data
33
35
  }
36
+
37
+ static func errorName(for code: ASAuthorizationError.Code) -> String {
38
+ switch code {
39
+ case .canceled, .failed, .notHandled:
40
+ return "NotAllowedError"
41
+ case .matchedExcludedCredential:
42
+ return "InvalidStateError"
43
+ default:
44
+ return "UnknownError"
45
+ }
46
+ }
47
+
48
+ static func nativeErrorDetails(from error: Error) -> JSObject? {
49
+ guard let authorizationError = error as? ASAuthorizationError else {
50
+ return nil
51
+ }
52
+
53
+ let nsError = error as NSError
54
+ var details: JSObject = [
55
+ "platform": "ios",
56
+ "code": authorizationError.code.rawValue,
57
+ "domain": nsError.domain
58
+ ]
59
+
60
+ if let reason = nsError.userInfo[NSLocalizedFailureReasonErrorKey] as? String {
61
+ details["failureReason"] = reason
62
+ }
63
+
64
+ return details
65
+ }
34
66
  }
35
67
 
36
68
  struct PasskeyPluginError: Error {
@@ -119,9 +119,9 @@ private func normalizeDomain(_ value: String?) -> String? {
119
119
 
120
120
  @objc(CapacitorPasskeyPlugin)
121
121
  public class CapacitorPasskeyPlugin: CAPPlugin,
122
- CAPBridgedPlugin,
123
- ASAuthorizationControllerDelegate,
124
- ASAuthorizationControllerPresentationContextProviding {
122
+ CAPBridgedPlugin,
123
+ ASAuthorizationControllerDelegate,
124
+ ASAuthorizationControllerPresentationContextProviding {
125
125
  public let identifier = "CapacitorPasskeyPlugin"
126
126
  public let jsName = "CapacitorPasskey"
127
127
  public let pluginMethods: [CAPPluginMethod] = [
@@ -166,7 +166,12 @@ public class CapacitorPasskeyPlugin: CAPPlugin,
166
166
  let assertionRequest = try JSONDecoder().decode(PasskeyAssertionRequestJSON.self, from: requestJson)
167
167
  let origin = normalizedOrigin(call.getString("origin"), rpId: assertionRequest.rpId)
168
168
  let authorizationRequest = try buildAssertionRequest(from: assertionRequest, origin: origin)
169
- performAuthorization(call: call, request: authorizationRequest, operation: .assertion)
169
+ performAuthorization(
170
+ call: call,
171
+ request: authorizationRequest,
172
+ operation: .assertion,
173
+ preferImmediatelyAvailableCredentials: call.getBool("preferImmediatelyAvailableCredentials") ?? false
174
+ )
170
175
  } catch let error as PasskeyPluginError {
171
176
  reject(call, error: error)
172
177
  } catch {
@@ -237,14 +242,17 @@ public class CapacitorPasskeyPlugin: CAPPlugin,
237
242
 
238
243
  clearPendingRequest()
239
244
  if let authorizationError = error as? ASAuthorizationError {
240
- switch authorizationError.code {
241
- case .canceled, .failed, .notHandled:
242
- reject(call, name: "NotAllowedError", message: authorizationError.localizedDescription, error: error)
243
- case .matchedExcludedCredential:
244
- reject(call, name: "InvalidStateError", message: authorizationError.localizedDescription, error: error)
245
- default:
246
- reject(call, name: "UnknownError", message: authorizationError.localizedDescription, error: error)
245
+ var data: JSObject = [:]
246
+ if let native = CapacitorPasskey.nativeErrorDetails(from: error) {
247
+ data["native"] = native
247
248
  }
249
+ reject(
250
+ call,
251
+ name: CapacitorPasskey.errorName(for: authorizationError.code),
252
+ message: authorizationError.localizedDescription,
253
+ error: error,
254
+ data: data
255
+ )
248
256
  return
249
257
  }
250
258
 
@@ -255,7 +263,12 @@ public class CapacitorPasskeyPlugin: CAPPlugin,
255
263
  bridge?.viewController?.view.window ?? ASPresentationAnchor()
256
264
  }
257
265
 
258
- private func performAuthorization(call: CAPPluginCall, request: ASAuthorizationRequest, operation: PasskeyOperation) {
266
+ private func performAuthorization(
267
+ call: CAPPluginCall,
268
+ request: ASAuthorizationRequest,
269
+ operation: PasskeyOperation,
270
+ preferImmediatelyAvailableCredentials: Bool = false
271
+ ) {
259
272
  pendingCall = call
260
273
  pendingOperation = operation
261
274
 
@@ -263,7 +276,11 @@ public class CapacitorPasskeyPlugin: CAPPlugin,
263
276
  let controller = ASAuthorizationController(authorizationRequests: [request])
264
277
  controller.delegate = self
265
278
  controller.presentationContextProvider = self
266
- controller.performRequests()
279
+ if #available(iOS 16.0, *), preferImmediatelyAvailableCredentials {
280
+ controller.performRequests(options: .preferImmediatelyAvailableCredentials)
281
+ } else {
282
+ controller.performRequests()
283
+ }
267
284
  }
268
285
  }
269
286
 
@@ -381,8 +398,15 @@ public class CapacitorPasskeyPlugin: CAPPlugin,
381
398
  reject(call, name: error.name, message: error.message)
382
399
  }
383
400
 
384
- private func reject(_ call: CAPPluginCall, name: String, message: String, error: Error? = nil) {
385
- let data: JSObject = ["name": name]
386
- call.reject(message, name, error, data)
401
+ private func reject(
402
+ _ call: CAPPluginCall,
403
+ name: String,
404
+ message: String,
405
+ error: Error? = nil,
406
+ data: JSObject = [:]
407
+ ) {
408
+ var payload = data
409
+ payload["name"] = name
410
+ call.reject(message, name, error, payload)
387
411
  }
388
412
  }
@@ -1,3 +1,4 @@
1
+ import AuthenticationServices
1
2
  import XCTest
2
3
  @testable import CapacitorPasskeyPlugin
3
4
 
@@ -14,4 +15,56 @@ class CapacitorPasskeyPluginTests: XCTestCase {
14
15
 
15
16
  XCTAssertEqual(input, decoded)
16
17
  }
18
+
19
+ func testErrorNameMatrix() {
20
+ let cases: [(ASAuthorizationError.Code, String)] = [
21
+ (.canceled, "NotAllowedError"),
22
+ (.failed, "NotAllowedError"),
23
+ (.notHandled, "NotAllowedError"),
24
+ ]
25
+
26
+ for (code, expected) in cases {
27
+ XCTAssertEqual(
28
+ CapacitorPasskey.errorName(for: code),
29
+ expected,
30
+ "ASAuthorizationError.Code rawValue \(code.rawValue)"
31
+ )
32
+ }
33
+
34
+ if #available(iOS 18.0, *) {
35
+ XCTAssertEqual(
36
+ CapacitorPasskey.errorName(for: .matchedExcludedCredential),
37
+ "InvalidStateError"
38
+ )
39
+ }
40
+ }
41
+
42
+ func testNativeErrorDetailsMatrix() throws {
43
+ let associationFailure = NSError(
44
+ domain: ASAuthorizationError.errorDomain,
45
+ code: ASAuthorizationError.Code.failed.rawValue,
46
+ userInfo: [NSLocalizedFailureReasonErrorKey: "Unable to verify webcredentials association"]
47
+ )
48
+ let associationDetails = try XCTUnwrap(CapacitorPasskey.nativeErrorDetails(from: associationFailure))
49
+ XCTAssertEqual(associationDetails["code"] as? Int, 1004)
50
+ XCTAssertEqual(associationDetails["platform"] as? String, "ios")
51
+ XCTAssertEqual(associationDetails["domain"] as? String, ASAuthorizationError.errorDomain)
52
+ XCTAssertEqual(
53
+ associationDetails["failureReason"] as? String,
54
+ "Unable to verify webcredentials association"
55
+ )
56
+
57
+ let canceled = NSError(
58
+ domain: ASAuthorizationError.errorDomain,
59
+ code: ASAuthorizationError.Code.canceled.rawValue,
60
+ userInfo: [:]
61
+ )
62
+ let canceledDetails = try XCTUnwrap(CapacitorPasskey.nativeErrorDetails(from: canceled))
63
+ XCTAssertEqual(canceledDetails["code"] as? Int, 1001)
64
+ XCTAssertEqual(canceledDetails["platform"] as? String, "ios")
65
+ XCTAssertNil(canceledDetails["failureReason"])
66
+
67
+ let unrelated = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil)
68
+ XCTAssertNil(CapacitorPasskey.nativeErrorDetails(from: unrelated))
69
+ }
17
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-passkey",
3
- "version": "8.3.10",
3
+ "version": "8.5.1",
4
4
  "description": "Capacitor passkey plugin with a WebAuthn-style shim for Capacitor apps.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",