@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.2 → 1.0.0-canary.21

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 (66) hide show
  1. package/.nvmrc +1 -0
  2. package/.oxfmtrc.json +4 -0
  3. package/.oxlintrc.json +14 -0
  4. package/.releaserc +30 -0
  5. package/ARCHITECTURE.md +8 -0
  6. package/CHANGELOG.md +115 -0
  7. package/CONTRIBUTING.md +15 -5
  8. package/README.md +150 -11
  9. package/android/build.gradle +33 -10
  10. package/android/libs/repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.pom +9 -0
  11. package/android/src/main/AndroidManifest.xml +9 -3
  12. package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +396 -44
  13. package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +583 -44
  14. package/android/src/main/java/co/algorand/passkeyautofill/Prf.kt +184 -0
  15. package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +190 -2
  16. package/android/src/main/java/co/algorand/passkeyautofill/WebAuthn.kt +135 -0
  17. package/android/src/main/java/co/algorand/passkeyautofill/auth/BiometricRequirement.kt +86 -0
  18. package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +2 -1
  19. package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +314 -53
  20. package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +152 -38
  21. package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +119 -0
  22. package/android/src/main/res/values/styles.xml +10 -0
  23. package/app.plugin.js +593 -60
  24. package/assets/banner.png +0 -0
  25. package/build/ReactNativePasskeyAutofill.types.d.ts +41 -1
  26. package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
  27. package/build/ReactNativePasskeyAutofill.types.js +3 -1
  28. package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
  29. package/build/ReactNativePasskeyAutofillModule.d.ts +41 -4
  30. package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -1
  31. package/build/ReactNativePasskeyAutofillModule.js +21 -2
  32. package/build/ReactNativePasskeyAutofillModule.js.map +1 -1
  33. package/build/index.d.ts +2 -2
  34. package/build/index.d.ts.map +1 -1
  35. package/build/index.js +2 -2
  36. package/build/index.js.map +1 -1
  37. package/e2e/README.md +155 -0
  38. package/e2e/driver/adb.ts +73 -0
  39. package/e2e/driver/capabilities.ts +106 -0
  40. package/e2e/driver/credentialProvider.ts +104 -0
  41. package/e2e/driver/fingerprint.ts +885 -0
  42. package/e2e/driver/selectors.ts +103 -0
  43. package/e2e/driver/session.ts +25 -0
  44. package/e2e/jest.config.ts +20 -0
  45. package/e2e/package.json +11 -0
  46. package/e2e/tests/passkey.test.ts +190 -0
  47. package/e2e/tsconfig.json +16 -0
  48. package/ios/AutofillCredentialProvider/BiometricRequirement.swift +34 -0
  49. package/ios/AutofillCredentialProvider/CredentialProviderViewController.swift +570 -0
  50. package/ios/AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h +1 -0
  51. package/ios/AutofillCredentialProvider/PasskeyCredentialStore.swift +612 -0
  52. package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.h +25 -0
  53. package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.mm +86 -0
  54. package/ios/AutofillCredentialProvider/Prf.swift +89 -0
  55. package/ios/AutofillCredentialProvider/WebAuthn.swift +186 -0
  56. package/ios/ReactNativePasskeyAutofill.podspec +11 -2
  57. package/ios/ReactNativePasskeyAutofillModule.swift +170 -15
  58. package/lefthook.yml +14 -0
  59. package/package.json +115 -25
  60. package/pnpm-workspace.yaml +36 -0
  61. package/src/ReactNativePasskeyAutofill.types.ts +38 -0
  62. package/src/ReactNativePasskeyAutofillModule.ts +66 -3
  63. package/src/index.ts +2 -2
  64. package/tsconfig.json +2 -2
  65. package/.eslintrc.js +0 -5
  66. /package/android/libs/{dP256Android-release.aar → repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.aar} +0 -0
@@ -0,0 +1,86 @@
1
+ #import "PasskeyKeystoreMMKV.h"
2
+
3
+ #import <MMKVCore/MMKV.h>
4
+
5
+ @implementation PasskeyKeystoreMMKV
6
+
7
+ + (nullable mmkv::MMKV *)keystoreForAppGroup:(NSString *)appGroup
8
+ error:(NSError * _Nullable * _Nullable)error {
9
+ NSURL *containerURL = [[NSFileManager defaultManager]
10
+ containerURLForSecurityApplicationGroupIdentifier:appGroup];
11
+ if (containerURL == nil) {
12
+ if (error != nil) {
13
+ *error = [NSError errorWithDomain:@"ReactNativePasskeyAutofill"
14
+ code:2
15
+ userInfo:@{
16
+ NSLocalizedDescriptionKey:
17
+ [NSString stringWithFormat:@"App Group is not accessible: %@", appGroup]
18
+ }];
19
+ }
20
+ return nil;
21
+ }
22
+
23
+ std::string rootPath([containerURL.path UTF8String]);
24
+ mmkv::MMKV::initializeMMKV(rootPath);
25
+ return mmkv::MMKV::mmkvWithID("keystore", mmkv::MMKV_MULTI_PROCESS, nullptr, &rootPath);
26
+ }
27
+
28
+ + (BOOL)setString:(NSString *)value
29
+ forKey:(NSString *)key
30
+ appGroup:(NSString *)appGroup
31
+ error:(NSError * _Nullable * _Nullable)error {
32
+ mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
33
+ if (keystore == nullptr) {
34
+ return NO;
35
+ }
36
+
37
+ std::string cppValue([value UTF8String]);
38
+ std::string cppKey([key UTF8String]);
39
+ return keystore->set(cppValue, cppKey);
40
+ }
41
+
42
+ + (nullable NSString *)stringForKey:(NSString *)key
43
+ appGroup:(NSString *)appGroup
44
+ error:(NSError * _Nullable * _Nullable)error {
45
+ mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
46
+ if (keystore == nullptr) {
47
+ return nil;
48
+ }
49
+
50
+ std::string result;
51
+ std::string cppKey([key UTF8String]);
52
+ if (!keystore->getString(cppKey, result)) {
53
+ return nil;
54
+ }
55
+ return [NSString stringWithUTF8String:result.c_str()];
56
+ }
57
+
58
+ + (NSArray<NSString *> *)allKeysForAppGroup:(NSString *)appGroup
59
+ error:(NSError * _Nullable * _Nullable)error {
60
+ mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
61
+ if (keystore == nullptr) {
62
+ return @[];
63
+ }
64
+
65
+ std::vector<std::string> keys = keystore->allKeys();
66
+ NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:keys.size()];
67
+ for (const auto &key : keys) {
68
+ [result addObject:[NSString stringWithUTF8String:key.c_str()]];
69
+ }
70
+ return result;
71
+ }
72
+
73
+ + (BOOL)removeValueForKey:(NSString *)key
74
+ appGroup:(NSString *)appGroup
75
+ error:(NSError * _Nullable * _Nullable)error {
76
+ mmkv::MMKV *keystore = [self keystoreForAppGroup:appGroup error:error];
77
+ if (keystore == nullptr) {
78
+ return NO;
79
+ }
80
+
81
+ std::string cppKey([key UTF8String]);
82
+ keystore->removeValueForKey(cppKey);
83
+ return YES;
84
+ }
85
+
86
+ @end
@@ -0,0 +1,89 @@
1
+ import CryptoKit
2
+ import Foundation
3
+
4
+ /// WebAuthn PRF (`prf`) extension helpers, backed by the authenticator
5
+ /// `hmac-secret` mechanism (CTAP2.1).
6
+ ///
7
+ /// Per the WebAuthn spec, for each input salt the authenticator computes:
8
+ ///
9
+ /// output = HMAC-SHA256(credRandom, SHA256("WebAuthn PRF" || 0x00 || salt))
10
+ ///
11
+ /// where `credRandom` is a 32-byte per-credential secret. To avoid a storage
12
+ /// migration we derive `credRandom` deterministically from the wallet's HD
13
+ /// root secret and the credential's (rpId, userHandle) — the same inputs that
14
+ /// already produce the deterministic P-256 signing key. As a result, restoring
15
+ /// the wallet seed on another device produces the same PRF outputs.
16
+ enum Prf {
17
+ /// Domain separator used to derive `credRandom` from the HD root secret.
18
+ /// Kept under a distinct label from the P-256 key derivation to ensure
19
+ /// the credRandom value can never collide with private-key material.
20
+ static let credRandomInfo = "WebAuthn-PRF-credRandom"
21
+
22
+ /// WebAuthn-defined prefix for PRF salt hashing.
23
+ /// See: https://www.w3.org/TR/webauthn-3/#prf-extension
24
+ static let saltPrefix: [UInt8] = Array("WebAuthn PRF".utf8) + [0x00]
25
+
26
+ /// Derive a 32-byte per-credential PRF secret (`credRandom`) from the wallet
27
+ /// HD root secret. Deterministic over `(hdRootSecret, rpId, userHandle)`.
28
+ static func credRandom(
29
+ hdRootSecret: Data,
30
+ relyingPartyIdentifier: String,
31
+ userHandle: String
32
+ ) -> Data {
33
+ var salt = Data()
34
+ salt.append(contentsOf: relyingPartyIdentifier.lowercased().utf8)
35
+ salt.append(0x00)
36
+ salt.append(contentsOf: userHandle.lowercased().utf8)
37
+
38
+ let info = Data(credRandomInfo.utf8)
39
+ let key = SymmetricKey(data: hdRootSecret)
40
+ let derived = HKDF<SHA256>.deriveKey(
41
+ inputKeyMaterial: key,
42
+ salt: salt,
43
+ info: info,
44
+ outputByteCount: 32
45
+ )
46
+ return derived.withUnsafeBytes { Data($0) }
47
+ }
48
+
49
+ /// Evaluate the PRF for a single salt. Returns 32 bytes of output material.
50
+ ///
51
+ /// Per the WebAuthn spec the PRF input the authenticator sees is the
52
+ /// hash `SHA-256("WebAuthn PRF" || 0x00 || rpSalt)`. The platform layer
53
+ /// (Apple AuthenticationServices on iOS, the system credential manager on
54
+ /// Android with `prfAlreadyHashed`) performs this hashing before passing
55
+ /// the salt down to credential providers. Pass `alreadyHashed = true` in
56
+ /// that case so we don't hash twice.
57
+ static func evaluate(credRandom: Data, salt: Data, alreadyHashed: Bool = true) -> Data {
58
+ let macInput: Data
59
+ if alreadyHashed {
60
+ macInput = salt
61
+ } else {
62
+ var hashInput = Data(saltPrefix)
63
+ hashInput.append(salt)
64
+ macInput = Data(SHA256.hash(data: hashInput))
65
+ }
66
+ let mac = HMAC<SHA256>.authenticationCode(
67
+ for: macInput,
68
+ using: SymmetricKey(data: credRandom)
69
+ )
70
+ return Data(mac)
71
+ }
72
+ }
73
+
74
+ /// Parsed PRF extension input as supplied by the relying party.
75
+ ///
76
+ /// At the WebAuthn layer the RP can either supply a single global pair of
77
+ /// salts (`eval`) or a per-credential map (`evalByCredential`). We always
78
+ /// reduce that down to at most two salts for the credential we are about to
79
+ /// assert with, matching the platform behaviour on iOS 18 and Android.
80
+ struct PrfInput {
81
+ let first: Data
82
+ let second: Data?
83
+ }
84
+
85
+ /// Computed PRF outputs ready to attach to a WebAuthn response.
86
+ struct PrfOutput {
87
+ let first: Data
88
+ let second: Data?
89
+ }
@@ -0,0 +1,186 @@
1
+ import CryptoKit
2
+ import Foundation
3
+
4
+ enum WebAuthnError: LocalizedError {
5
+ case invalidP256PublicKeyLength(Int)
6
+
7
+ var errorDescription: String? {
8
+ switch self {
9
+ case .invalidP256PublicKeyLength(let length):
10
+ return "Invalid P-256 public key length: \(length)."
11
+ }
12
+ }
13
+ }
14
+
15
+ enum WebAuthn {
16
+ private static let aaguidInfoDictionaryKey = "ReactNativePasskeyAutofillAAGUID"
17
+ private static let defaultAaguid = UUID(uuidString: "1F59713A-C021-4E63-9158-2CC5FDC14E52")!
18
+
19
+ // Injected by the Expo config plugin (`aaguid` prop) via the extension Info.plist so all
20
+ // platforms can present the same authenticator identity; falls back to the module default.
21
+ static let aaguid: UUID = {
22
+ guard
23
+ let raw = Bundle.main.object(forInfoDictionaryKey: aaguidInfoDictionaryKey) as? String,
24
+ let configured = UUID(uuidString: raw)
25
+ else {
26
+ return defaultAaguid
27
+ }
28
+ return configured
29
+ }()
30
+
31
+ static func authenticatorDataForAssertion(relyingPartyIdentifier: String) -> Data {
32
+ var data = Data(SHA256.hash(data: Data(relyingPartyIdentifier.utf8)))
33
+ data.append(flags(attestedCredentialDataIncluded: false))
34
+ data.append(signCount())
35
+ return data
36
+ }
37
+
38
+ static func authenticatorDataForAttestation(
39
+ relyingPartyIdentifier: String,
40
+ credentialId: Data,
41
+ publicKey: Data
42
+ ) throws -> Data {
43
+ var data = Data(SHA256.hash(data: Data(relyingPartyIdentifier.utf8)))
44
+ data.append(flags(attestedCredentialDataIncluded: true))
45
+ data.append(signCount())
46
+ data.append(try attestedCredentialData(credentialId: credentialId, publicKey: publicKey))
47
+ return data
48
+ }
49
+
50
+ static func attestationObject(authenticatorData: Data) -> Data {
51
+ Cbor.encodeMap([
52
+ (Cbor.encodeText("fmt"), Cbor.encodeText("none")),
53
+ (Cbor.encodeText("attStmt"), Cbor.encodeMap([])),
54
+ (Cbor.encodeText("authData"), Cbor.encodeBytes(authenticatorData)),
55
+ ])
56
+ }
57
+
58
+ static func credentialId(publicKey: Data) -> Data {
59
+ Data(SHA256.hash(data: publicKey))
60
+ }
61
+
62
+ private static func flags(attestedCredentialDataIncluded: Bool) -> Data {
63
+ var flags: UInt8 = 0x01
64
+ flags |= 0x04
65
+ flags |= 0x08
66
+ flags |= 0x10
67
+ if attestedCredentialDataIncluded {
68
+ flags |= 0x40
69
+ }
70
+ return Data([flags])
71
+ }
72
+
73
+ private static func signCount() -> Data {
74
+ Data([0x00, 0x00, 0x00, 0x00])
75
+ }
76
+
77
+ private static func attestedCredentialData(credentialId: Data, publicKey: Data) throws -> Data {
78
+ var data = aaguid.bytes
79
+ data.append(UInt8((credentialId.count >> 8) & 0xff))
80
+ data.append(UInt8(credentialId.count & 0xff))
81
+ data.append(credentialId)
82
+ data.append(try coseKey(publicKey: publicKey))
83
+ return data
84
+ }
85
+
86
+ private static func coseKey(publicKey: Data) throws -> Data {
87
+ let coordinates = try p256Coordinates(publicKey: publicKey)
88
+
89
+ return Cbor.encodeMap([
90
+ (Cbor.encodeInt(1), Cbor.encodeInt(2)),
91
+ (Cbor.encodeInt(3), Cbor.encodeInt(-7)),
92
+ (Cbor.encodeInt(-1), Cbor.encodeInt(1)),
93
+ (Cbor.encodeInt(-2), Cbor.encodeBytes(coordinates.x)),
94
+ (Cbor.encodeInt(-3), Cbor.encodeBytes(coordinates.y)),
95
+ ])
96
+ }
97
+
98
+ private static func p256Coordinates(publicKey: Data) throws -> (x: Data, y: Data) {
99
+ if let derPublicKey = try? P256.Signing.PublicKey(derRepresentation: publicKey) {
100
+ return try p256Coordinates(publicKey: derPublicKey.x963Representation)
101
+ }
102
+
103
+ if publicKey.count == 65, publicKey.first == 0x04 {
104
+ let coordinates = publicKey.dropFirst()
105
+ return (
106
+ Data(coordinates.prefix(32)),
107
+ Data(coordinates.dropFirst(32).prefix(32))
108
+ )
109
+ }
110
+
111
+ if publicKey.count == 64 {
112
+ return (
113
+ Data(publicKey.prefix(32)),
114
+ Data(publicKey.dropFirst(32).prefix(32))
115
+ )
116
+ }
117
+
118
+ throw WebAuthnError.invalidP256PublicKeyLength(publicKey.count)
119
+ }
120
+ }
121
+
122
+ private enum Cbor {
123
+ static func encodeInt(_ value: Int) -> Data {
124
+ if value >= 0 {
125
+ return encodeUnsigned(UInt64(value), majorType: 0)
126
+ }
127
+ return encodeUnsigned(UInt64(-1 - value), majorType: 1)
128
+ }
129
+
130
+ static func encodeBytes(_ bytes: Data) -> Data {
131
+ encodeUnsigned(UInt64(bytes.count), majorType: 2) + bytes
132
+ }
133
+
134
+ static func encodeText(_ string: String) -> Data {
135
+ let bytes = Data(string.utf8)
136
+ return encodeUnsigned(UInt64(bytes.count), majorType: 3) + bytes
137
+ }
138
+
139
+ static func encodeMap(_ pairs: [(Data, Data)]) -> Data {
140
+ var data = encodeUnsigned(UInt64(pairs.count), majorType: 5)
141
+ for (key, value) in pairs {
142
+ data.append(key)
143
+ data.append(value)
144
+ }
145
+ return data
146
+ }
147
+
148
+ private static func encodeUnsigned(_ value: UInt64, majorType: UInt8) -> Data {
149
+ let prefix = majorType << 5
150
+ switch value {
151
+ case 0..<24:
152
+ return Data([prefix | UInt8(value)])
153
+ case 24..<256:
154
+ return Data([prefix | 24, UInt8(value)])
155
+ case 256..<65_536:
156
+ return Data([prefix | 25, UInt8((value >> 8) & 0xff), UInt8(value & 0xff)])
157
+ case 65_536..<4_294_967_296:
158
+ return Data([
159
+ prefix | 26,
160
+ UInt8((value >> 24) & 0xff),
161
+ UInt8((value >> 16) & 0xff),
162
+ UInt8((value >> 8) & 0xff),
163
+ UInt8(value & 0xff),
164
+ ])
165
+ default:
166
+ return Data([
167
+ prefix | 27,
168
+ UInt8((value >> 56) & 0xff),
169
+ UInt8((value >> 48) & 0xff),
170
+ UInt8((value >> 40) & 0xff),
171
+ UInt8((value >> 32) & 0xff),
172
+ UInt8((value >> 24) & 0xff),
173
+ UInt8((value >> 16) & 0xff),
174
+ UInt8((value >> 8) & 0xff),
175
+ UInt8(value & 0xff),
176
+ ])
177
+ }
178
+ }
179
+ }
180
+
181
+ private extension UUID {
182
+ var bytes: Data {
183
+ var uuid = uuid
184
+ return withUnsafeBytes(of: &uuid) { Data($0) }
185
+ }
186
+ }
@@ -19,11 +19,20 @@ Pod::Spec.new do |s|
19
19
  s.static_framework = true
20
20
 
21
21
  s.dependency 'ExpoModulesCore'
22
+ s.dependency 'MMKVCore'
22
23
 
23
- # Swift/Objective-C compatibility
24
24
  s.pod_target_xcconfig = {
25
25
  'DEFINES_MODULE' => 'YES',
26
26
  }
27
27
 
28
- s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
28
+ s.source_files = [
29
+ "*.{h,m,mm,swift,hpp,cpp}",
30
+ "AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h",
31
+ "AutofillCredentialProvider/PasskeyCredentialStore.swift",
32
+ "AutofillCredentialProvider/PasskeyKeystoreMMKV.h",
33
+ "AutofillCredentialProvider/PasskeyKeystoreMMKV.mm"
34
+ ]
35
+ s.public_header_files = [
36
+ "AutofillCredentialProvider/PasskeyKeystoreMMKV.h"
37
+ ]
29
38
  end
@@ -1,4 +1,8 @@
1
1
  import ExpoModulesCore
2
+ import AuthenticationServices
3
+ #if canImport(UIKit)
4
+ import UIKit
5
+ #endif
2
6
 
3
7
  public class ReactNativePasskeyAutofillModule: Module {
4
8
  // Each module class must implement the definition function. The definition consists of components
@@ -10,26 +14,177 @@ public class ReactNativePasskeyAutofillModule: Module {
10
14
  // The module will be accessible from `requireNativeModule('ReactNativePasskeyAutofill')` in JavaScript.
11
15
  Name("ReactNativePasskeyAutofill")
12
16
 
13
- // Defines constant property on the module.
14
- Constant("PI") {
15
- Double.pi
17
+ Events("onPasskeyAdded", "onPasskeyAuthenticated")
18
+
19
+ AsyncFunction("setMasterKey") { (secret: String) in
20
+ guard let store = PasskeyCredentialStore() else {
21
+ throw NSError(
22
+ domain: "ReactNativePasskeyAutofill",
23
+ code: 1,
24
+ userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
25
+ )
26
+ }
27
+ store.saveMasterKey(secret)
28
+ }
29
+
30
+ AsyncFunction("setHdRootKeyId") { (id: String) in
31
+ guard let store = PasskeyCredentialStore() else {
32
+ throw NSError(
33
+ domain: "ReactNativePasskeyAutofill",
34
+ code: 1,
35
+ userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
36
+ )
37
+ }
38
+ store.saveHdRootKeyId(id)
39
+ }
40
+
41
+ AsyncFunction("getHdRootKeyId") { () -> String? in
42
+ guard let store = PasskeyCredentialStore() else {
43
+ return nil
44
+ }
45
+ return store.hdRootKeyId()
46
+ }
47
+
48
+ AsyncFunction("clearCredentials") {
49
+ guard let store = PasskeyCredentialStore() else {
50
+ return
51
+ }
52
+ store.clear()
53
+ try await store.removeAllIdentities()
54
+ }
55
+
56
+ AsyncFunction("deleteCredential") { (credentialId: String) in
57
+ guard let store = PasskeyCredentialStore() else {
58
+ return
59
+ }
60
+ try store.removeCredential(id: credentialId)
61
+ try await store.replaceIdentityStore()
62
+ }
63
+
64
+ AsyncFunction("configureIntentActions") { (getPasskeyAction: String, createPasskeyAction: String) in
65
+ guard let store = PasskeyCredentialStore() else {
66
+ return
67
+ }
68
+ store.configureIntentActions(
69
+ getPasskeyAction: getPasskeyAction,
70
+ createPasskeyAction: createPasskeyAction
71
+ )
72
+ }
73
+
74
+ AsyncFunction("replaceCredentialIdentities") { (credentials: [[String: Any]]) in
75
+ guard let store = PasskeyCredentialStore() else {
76
+ throw NSError(
77
+ domain: "ReactNativePasskeyAutofill",
78
+ code: 1,
79
+ userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
80
+ )
81
+ }
82
+
83
+ let storedCredentials = credentials.compactMap { credential -> StoredPasskeyCredential? in
84
+ guard let credentialId = credential["credentialId"] as? String ?? credential["id"] as? String,
85
+ let relyingPartyIdentifier =
86
+ credential["relyingPartyIdentifier"] as? String ??
87
+ credential["rpId"] as? String ??
88
+ credential["origin"] as? String,
89
+ let userName =
90
+ credential["userName"] as? String ??
91
+ credential["name"] as? String ??
92
+ credential["userHandle"] as? String,
93
+ let userHandle =
94
+ credential["userHandle"] as? String ??
95
+ credential["userId"] as? String,
96
+ let privateKey =
97
+ credential["privateKey"] as? String ??
98
+ credential["privateKeyBase64"] as? String
99
+ else {
100
+ return nil
101
+ }
102
+
103
+ let metadata = credential["metadata"] as? [String: Any]
104
+
105
+ return StoredPasskeyCredential(
106
+ credentialId: credentialId,
107
+ relyingPartyIdentifier: relyingPartyIdentifier.relyingPartyIdentifier,
108
+ userName: userName,
109
+ userHandle: userHandle,
110
+ privateKey: privateKey,
111
+ publicKey: credential["publicKey"] as? String ?? credential["publicKeyBase64"] as? String,
112
+ createdAt: credential["createdAt"] as? Double ?? Date().timeIntervalSince1970,
113
+ lastUsedAt: credential["lastUsedAt"] as? Double ?? metadata?["lastUsedAt"] as? Double,
114
+ parentKeyId: credential["parentKeyId"] as? String ?? metadata?["parentKeyId"] as? String
115
+ )
116
+ }
117
+
118
+ try store.replace(storedCredentials)
119
+ try await store.replaceIdentityStore()
120
+ }
121
+
122
+ AsyncFunction("refreshCredentialIdentities") {
123
+ guard let store = PasskeyCredentialStore() else {
124
+ throw NSError(
125
+ domain: "ReactNativePasskeyAutofill",
126
+ code: 1,
127
+ userInfo: [NSLocalizedDescriptionKey: "App Group is not configured for passkey autofill."]
128
+ )
129
+ }
130
+
131
+ try await store.replaceIdentityStore()
132
+ }
133
+
134
+ AsyncFunction("getStoredCredentials") { () -> [[String: Any]] in
135
+ guard let store = PasskeyCredentialStore() else {
136
+ return []
137
+ }
138
+
139
+ return store.allCredentials().map { credential in
140
+ var result: [String: Any] = [
141
+ "credentialId": credential.credentialId,
142
+ "relyingPartyIdentifier": credential.relyingPartyIdentifier,
143
+ "userName": credential.userName,
144
+ "userHandle": credential.userHandle,
145
+ "createdAt": credential.createdAt,
146
+ ]
147
+
148
+ if let publicKey = credential.publicKey {
149
+ result["publicKey"] = publicKey
150
+ }
151
+ if let lastUsedAt = credential.lastUsedAt {
152
+ result["lastUsedAt"] = lastUsedAt
153
+ }
154
+ if let parentKeyId = credential.parentKeyId {
155
+ result["parentKeyId"] = parentKeyId
156
+ }
157
+
158
+ return result
159
+ }
16
160
  }
17
161
 
18
- // Defines event names that the module can send to JavaScript.
19
- Events("onChange")
162
+ AsyncFunction("getDiagnostics") { () -> [String] in
163
+ guard let store = PasskeyCredentialStore() else {
164
+ return []
165
+ }
166
+ return store.diagnostics()
167
+ }
20
168
 
21
- // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
22
- Function("hello") {
23
- return "Hello world! 👋"
169
+ // Returns true when this app is the user-selected AutoFill credential
170
+ // provider. On iOS the Credential Provider extension reports its
171
+ // enablement via `ASCredentialIdentityStore.getState`.
172
+ AsyncFunction("isProviderActive") { (promise: Promise) in
173
+ ASCredentialIdentityStore.shared.getState { state in
174
+ promise.resolve(state.isEnabled)
175
+ }
24
176
  }
25
177
 
26
- // Defines a JavaScript function that always returns a Promise and whose native code
27
- // is by default dispatched on the different thread than the JavaScript runtime runs on.
28
- AsyncFunction("setValueAsync") { (value: String) in
29
- // Send an event to JavaScript.
30
- self.sendEvent("onChange", [
31
- "value": value
32
- ])
178
+ // Best-effort deep link to the iOS AutoFill settings screen.
179
+ AsyncFunction("openProviderSettings") { () -> Bool in
180
+ #if canImport(UIKit)
181
+ if let url = URL(string: UIApplication.openSettingsURLString),
182
+ UIApplication.shared.canOpenURL(url) {
183
+ UIApplication.shared.open(url)
184
+ return true
185
+ }
186
+ #endif
187
+ return false
33
188
  }
34
189
 
35
190
  // Enables the module to be used as a native view. Definition components that are accepted as part of the
package/lefthook.yml ADDED
@@ -0,0 +1,14 @@
1
+ # Lefthook configuration
2
+ # Refer to https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
3
+
4
+ pre-commit:
5
+ commands:
6
+ lint:
7
+ run: pnpm run lint
8
+ fmt:
9
+ run: pnpm run fmt
10
+ pre-push:
11
+ parallel: true
12
+ commands:
13
+ test:
14
+ run: pnpm run test:all