@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.4 โ†’ 1.0.0-canary.6

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/.eslintrc.js CHANGED
@@ -2,4 +2,7 @@ module.exports = {
2
2
  root: true,
3
3
  extends: ['universe/native', 'universe/web'],
4
4
  ignorePatterns: ['build'],
5
+ rules: {
6
+ 'node/handle-callback-err': 'off',
7
+ },
5
8
  };
package/.releaserc ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "branches": [
3
+ "release",
4
+ {
5
+ "name": "main",
6
+ "prerelease": "canary"
7
+ }
8
+ ],
9
+ "plugins": [
10
+ "@semantic-release/commit-analyzer",
11
+ "@semantic-release/release-notes-generator",
12
+ "@semantic-release/changelog",
13
+ "@semantic-release/npm",
14
+ [
15
+ "@semantic-release/git",
16
+ {
17
+ "assets": ["package.json", "CHANGELOG.md"],
18
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
19
+ }
20
+ ],
21
+ "@semantic-release/github"
22
+ ]
23
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # [1.0.0-canary.6](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.5...v1.0.0-canary.6) (2026-03-24)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * include banner for package README ([60a6559](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/60a655932faac6de57daf0f30261f2d03a2c1476))
7
+
8
+ # [1.0.0-canary.5](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.4...v1.0.0-canary.5) (2026-03-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * **android:** add allowed credentials mapping ([e7134fc](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/e7134fc1e5bf09649155e38010e4ef9728580812))
14
+ * **android:** credential compatibility with system-provided clientDataHash and compact JSON ([428e96f](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/428e96f89b73cd35fdbfaa6669f39da68403e17e))
package/CONTRIBUTING.md CHANGED
@@ -50,6 +50,16 @@ The project includes an example app in the `example/` directory. You can use it
50
50
 
51
51
  For more details on how the project is structured, see the [Architecture Guide](./ARCHITECTURE.md).
52
52
 
53
+ ### Testing
54
+
55
+ Before submitting a Pull Request, ensure that both JavaScript and Native tests pass:
56
+
57
+ ```bash
58
+ npm run test:all
59
+ ```
60
+
61
+ For more details on the testing strategy, refer to the [Testing section in README.md](./README.md#๐Ÿงช-testing).
62
+
53
63
  ## Submitting a Pull Request
54
64
 
55
65
  1. Create a new branch for your feature or bugfix:
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @algorandfoundation/react-native-passkey-autofill
2
2
 
3
3
  <p align="center">
4
- <img src="./example/assets/banner.png" width="100%" />
4
+ <img src="./assets/banner.png" width="100%" />
5
5
  </p>
6
6
 
7
7
  <p align="center">
@@ -66,6 +66,34 @@ await ReactNativePasskeyAutofill.configureIntentActions(
66
66
  await ReactNativePasskeyAutofill.clearCredentials();
67
67
  ```
68
68
 
69
+ ## ๐Ÿงช Testing
70
+
71
+ The project is set up with a comprehensive testing approach covering both JavaScript and Native (Kotlin) sides.
72
+
73
+ ### JavaScript Tests
74
+ Run unit tests for the TypeScript module using Jest:
75
+ ```bash
76
+ npm test
77
+ ```
78
+
79
+ ### Native Android Tests
80
+ Run unit tests for the Kotlin code using JUnit and Robolectric. These tests are executed via the example app's Gradle wrapper:
81
+ ```bash
82
+ npm run test:android
83
+ ```
84
+
85
+ ### All Tests
86
+ Run both JS and Native tests:
87
+ ```bash
88
+ npm run test:all
89
+ ```
90
+
91
+ ### Continuous Integration (CI)
92
+ The project includes a GitHub Actions workflow that automatically runs linting, JS tests, and Native Android tests on every push and pull request to the `main` or `release` branches. You can find the configuration in `.github/workflows/ci.yml`.
93
+
94
+ ### Integration Testing (E2E)
95
+ For full end-to-end testing that covers the bridge between JavaScript and Native, we recommend using [Maestro](https://maestro.mobile.dev/). It provides a clean, YAML-based way to automate UI flows and verify the integration of the Passkey Autofill service with the system UI.
96
+
69
97
  ## ๐Ÿ“ฑ Example App
70
98
 
71
99
  The [example](./example) app demonstrates how to integrate this module with a full wallet implementation.
@@ -27,6 +27,7 @@ if (useManagedAndroidSdkVersions) {
27
27
  defaultConfig {
28
28
  minSdkVersion safeExtGet("minSdkVersion", 24)
29
29
  targetSdkVersion safeExtGet("targetSdkVersion", 36)
30
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
30
31
  }
31
32
  }
32
33
  }
@@ -43,6 +44,12 @@ android {
43
44
  kotlinOptions {
44
45
  freeCompilerArgs += ["-opt-in=kotlin.time.ExperimentalTime", "-opt-in=kotlin.RequiresOptIn"]
45
46
  }
47
+ testOptions {
48
+ unitTests.includeAndroidResources = true
49
+ }
50
+ packagingOptions {
51
+ pickFirst '**/libc++_shared.so'
52
+ }
46
53
  }
47
54
 
48
55
  dependencies {
@@ -59,6 +66,17 @@ dependencies {
59
66
  implementation fileTree(dir: 'libs', include: ['*.jar'])
60
67
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
61
68
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0"
69
+
70
+ // Testing dependencies
71
+ testImplementation "junit:junit:4.13.2"
72
+ testImplementation "org.robolectric:robolectric:4.11.1"
73
+ testImplementation "io.mockk:mockk:1.13.8"
74
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0"
75
+
76
+ androidTestImplementation "androidx.test:runner:1.5.2"
77
+ androidTestImplementation "androidx.test:rules:1.5.0"
78
+ androidTestImplementation "androidx.test.ext:junit:1.1.5"
79
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
62
80
  }
63
81
 
64
82
  repositories {
@@ -6,7 +6,6 @@
6
6
  android:enabled="true"
7
7
  android:exported="true"
8
8
  android:label="@string/passkey_autofill_label"
9
- android:icon="@mipmap/ic_launcher"
10
9
  android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
11
10
  tools:targetApi="upside_down_cake">
12
11
  <intent-filter>
@@ -266,25 +266,34 @@ class CreatePasskeyActivity : AppCompatActivity() {
266
266
  )
267
267
 
268
268
  val resultIntent = Intent()
269
- val responseJson = JSONObject(fidoCredential.json())
270
- val passkeyDetails = JSONObject()
271
-
272
- val ecPubKey = keyPair.public as java.security.interfaces.ECPublicKey
273
- val ecPrivKey = keyPair.private as java.security.interfaces.ECPrivateKey
274
-
275
- fun to32(bi: java.math.BigInteger): ByteArray {
276
- var ba = bi.toByteArray()
277
- if (ba.size < 32) {
278
- ba = ByteArray(32 - ba.size) + ba
269
+ val fullJson = JSONObject(fidoCredential.json())
270
+ val respJson = fullJson.getJSONObject("response")
271
+
272
+ // Ensure compact clientDataJSON for registration too
273
+ val sanitizedOrigin = credentialRepository.getOrigin(req.callingAppInfo).replace(Regex("/$"), "")
274
+ val challenge = if (requestJson.has("publicKey")) {
275
+ requestJson.getJSONObject("publicKey").getString("challenge")
276
+ } else {
277
+ requestJson.getString("challenge")
278
+ }
279
+
280
+ val clientDataJSONString = if (sanitizedOrigin.startsWith("https://") || sanitizedOrigin.startsWith("http://")) {
281
+ "{\"type\":\"webauthn.create\",\"challenge\":\"$challenge\",\"origin\":\"$sanitizedOrigin\",\"crossOrigin\":false}"
282
+ } else {
283
+ val json = JSONObject()
284
+ json.put("type", "webauthn.create")
285
+ json.put("challenge", challenge)
286
+ json.put("origin", sanitizedOrigin)
287
+ json.put("crossOrigin", false)
288
+ if (sanitizedOrigin.startsWith("android:apk-key-hash:")) {
289
+ json.put("androidPackageName", req.callingAppInfo.packageName)
279
290
  }
280
- return ba.copyOfRange(ba.size - 32, ba.size)
291
+ json.toString()
281
292
  }
282
-
283
- val x = to32(ecPubKey.w.affineX)
284
- val y = to32(ecPubKey.w.affineY)
285
- val s = to32(ecPrivKey.s)
293
+ val clientDataJSONb64 = AndroidBase64.encodeToString(clientDataJSONString.toByteArray(), AndroidBase64.URL_SAFE or AndroidBase64.NO_WRAP or AndroidBase64.NO_PADDING)
294
+ respJson.put("clientDataJSON", clientDataJSONb64)
286
295
 
287
- val createResponse = CreatePublicKeyCredentialResponse(responseJson.toString())
296
+ val createResponse = CreatePublicKeyCredentialResponse(fullJson.toString())
288
297
 
289
298
  PendingIntentHandler.setCreateCredentialResponse(
290
299
  resultIntent,
@@ -229,17 +229,31 @@ class GetPasskeyActivity : AppCompatActivity() {
229
229
  }
230
230
  val sanitizedOrigin = origin.replace(Regex("/$"), "")
231
231
 
232
- val clientDataJSON = JSONObject()
233
- clientDataJSON.put("type", "webauthn.get")
234
- clientDataJSON.put("challenge", challenge)
235
- clientDataJSON.put("origin", sanitizedOrigin)
236
- clientDataJSON.put("crossOrigin", false)
237
- if (sanitizedOrigin.startsWith("android:apk-key-hash:")) {
238
- clientDataJSON.put("androidPackageName", req.callingAppInfo.packageName)
232
+ // Look for system-provided clientDataHash (e.g. from Chrome)
233
+ val systemClientDataHash = run {
234
+ val option = req.credentialOptions.find { opt ->
235
+ opt is GetPublicKeyCredentialOption && (opt.requestJson == rawRequestJson ||
236
+ try { JSONObject(opt.requestJson).toString() == JSONObject(rawRequestJson!!).toString() } catch(e: Exception) { false })
237
+ } as? GetPublicKeyCredentialOption
238
+ option?.requestData?.getByteArray("androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH")
239
239
  }
240
240
 
241
- val clientDataJSONString = clientDataJSON.toString()
242
- val clientDataHash = MessageDigest.getInstance("SHA-256").digest(clientDataJSONString.toByteArray(Charsets.UTF_8))
241
+ val clientDataJSONString = if (sanitizedOrigin.startsWith("https://") || sanitizedOrigin.startsWith("http://")) {
242
+ // Compact JSON for web origins to match browser hashing (no spaces, specific order)
243
+ "{\"type\":\"webauthn.get\",\"challenge\":\"$challenge\",\"origin\":\"$sanitizedOrigin\",\"crossOrigin\":false}"
244
+ } else {
245
+ val json = JSONObject()
246
+ json.put("type", "webauthn.get")
247
+ json.put("challenge", challenge)
248
+ json.put("origin", sanitizedOrigin)
249
+ json.put("crossOrigin", false)
250
+ if (sanitizedOrigin.startsWith("android:apk-key-hash:")) {
251
+ json.put("androidPackageName", req.callingAppInfo.packageName)
252
+ }
253
+ json.toString()
254
+ }
255
+
256
+ val clientDataHash = systemClientDataHash ?: MessageDigest.getInstance("SHA-256").digest(clientDataJSONString.toByteArray(Charsets.UTF_8))
243
257
 
244
258
  val dbCred = credentialRepository.getCredential(this, credId)
245
259
  ?: throw IllegalStateException("Credential not found")
@@ -26,8 +26,10 @@ import androidx.credentials.provider.ProviderClearCredentialStateRequest
26
26
  import androidx.credentials.provider.PublicKeyCredentialEntry
27
27
  import co.algorand.passkeyautofill.credentials.CredentialRepository
28
28
  import co.algorand.passkeyautofill.credentials.Credential
29
+ import co.algorand.passkeyautofill.utils.PasskeyUtils
29
30
  import com.tencent.mmkv.MMKV
30
31
  import kotlinx.coroutines.*
32
+ import org.json.JSONArray
31
33
  import org.json.JSONObject
32
34
 
33
35
  @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -127,7 +129,7 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
127
129
  /**
128
130
  * Fake a list of available PublicKeyCredential Entries
129
131
  */
130
- private fun processGetCredentialRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse{
132
+ private fun processGetCredentialRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse {
131
133
  // Ensure we have a master key available. If not, we can't sign anything, so don't show entries.
132
134
  if (!credentialRepository.isMasterKeyAvailable(this)) {
133
135
  return BeginGetCredentialResponse(emptyList())
@@ -138,42 +140,66 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
138
140
  }
139
141
 
140
142
  val action = credentialRepository.getGetPasskeyAction(this) ?: DEFAULT_GET_PASSKEY_ACTION
143
+ val allEntries = mutableListOf<PublicKeyCredentialEntry>()
141
144
 
142
- val entries = credentials.mapNotNull { credential ->
143
- try {
144
- // Find suitable option for this entry
145
- val option = request.beginGetCredentialOptions.find { opt ->
146
- if (opt !is BeginGetPublicKeyCredentialOption) return@find false
147
-
148
- // In a real app, we'd check if this credential's origin matches the request's origin
149
- // For now, we return all credentials that match the requested type.
150
- true
151
- } as? BeginGetPublicKeyCredentialOption
152
-
153
- if (option == null) {
154
- return@mapNotNull null
155
- }
145
+ for (option in request.beginGetCredentialOptions) {
146
+ if (option !is BeginGetPublicKeyCredentialOption) continue
156
147
 
157
- val data = Bundle()
158
- data.putString("credentialId", credential.credentialId)
159
- data.putString("userHandle", credential.userHandle)
160
- data.putString("requestJson", option.requestJson)
161
-
162
- PublicKeyCredentialEntry.Builder(
163
- this@PasskeyAutofillCredentialProviderService,
164
- credential.userHandle,
165
- createNewPendingIntent(action, GET_PASSKEY_INTENT, data),
166
- option
167
- )
168
- .setIcon(Icon.createWithResource(this@PasskeyAutofillCredentialProviderService, android.R.drawable.ic_lock_lock))
169
- .build()
148
+ val requestJsonStr = option.requestJson
149
+ var allowCredentials: JSONArray? = null
150
+ try {
151
+ val json = JSONObject(requestJsonStr)
152
+ val pk = if (json.has("publicKey")) json.getJSONObject("publicKey") else json
153
+ allowCredentials = pk.optJSONArray("allowCredentials")
170
154
  } catch (e: Exception) {
171
- Log.e(TAG, "Error building PublicKeyCredentialEntry", e)
155
+ Log.e(TAG, "Error parsing requestJson", e)
156
+ }
157
+
158
+ val allowedIds = if (allowCredentials != null && allowCredentials.length() > 0) {
159
+ val ids = mutableSetOf<String>()
160
+ for (i in 0 until allowCredentials.length()) {
161
+ ids.add(allowCredentials.getJSONObject(i).getString("id"))
162
+ }
163
+ ids
164
+ } else {
172
165
  null
173
166
  }
167
+
168
+ for (credential in credentials) {
169
+ if (allowedIds != null) {
170
+ val isAllowed = allowedIds.any { allowedId ->
171
+ allowedId == credential.credentialId ||
172
+ PasskeyUtils.normalizeBase64(allowedId) == PasskeyUtils.normalizeBase64(credential.credentialId)
173
+ }
174
+ if (!isAllowed) continue
175
+ }
176
+
177
+ try {
178
+ val data = Bundle()
179
+ data.putString("credentialId", credential.credentialId)
180
+ data.putString("userHandle", credential.userHandle)
181
+ data.putString("requestJson", option.requestJson)
182
+
183
+ // Use a unique requestCode for each PendingIntent to avoid data being overwritten
184
+ val uniqueRequestCode = (credential.credentialId + option.requestJson).hashCode()
185
+
186
+ val entry = PublicKeyCredentialEntry.Builder(
187
+ this,
188
+ credential.userHandle,
189
+ createNewPendingIntent(action, uniqueRequestCode, data),
190
+ option
191
+ )
192
+ .setIcon(Icon.createWithResource(this, android.R.drawable.ic_lock_lock))
193
+ .build()
194
+ allEntries.add(entry)
195
+ } catch (e: Exception) {
196
+ Log.e(TAG, "Error building PublicKeyCredentialEntry", e)
197
+ }
198
+ }
174
199
  }
175
- return BeginGetCredentialResponse(entries)
200
+ return BeginGetCredentialResponse(allEntries)
176
201
  }
202
+
177
203
  override fun onClearCredentialStateRequest(
178
204
  request: ProviderClearCredentialStateRequest,
179
205
  cancellationSignal: CancellationSignal,
@@ -186,12 +212,17 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
186
212
  private fun createNewPendingIntent(action: String, requestCode: Int, extra: Bundle?): PendingIntent{
187
213
  MMKV.initialize(this)
188
214
  val intent = Intent(action)
189
- // Set component to be explicit to this library's activities
190
- val componentName = when(requestCode) {
191
- GET_PASSKEY_INTENT -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.GetPasskeyActivity")
192
- CREATE_PASSKEY_INTENT -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.CreatePasskeyActivity")
215
+
216
+ // Determine component based on action instead of requestCode
217
+ val getAction = credentialRepository.getGetPasskeyAction(this) ?: DEFAULT_GET_PASSKEY_ACTION
218
+ val createAction = credentialRepository.getCreatePasskeyAction(this) ?: DEFAULT_CREATE_PASSKEY_ACTION
219
+
220
+ val componentName = when(action) {
221
+ getAction -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.GetPasskeyActivity")
222
+ createAction -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.CreatePasskeyActivity")
193
223
  else -> null
194
224
  }
225
+
195
226
  if (componentName != null) {
196
227
  intent.component = componentName
197
228
  } else {
@@ -0,0 +1,7 @@
1
+ package co.algorand.passkeyautofill.utils
2
+
3
+ object PasskeyUtils {
4
+ fun normalizeBase64(s: String): String {
5
+ return s.replace("-", "+").replace("_", "/").replace("=", "")
6
+ }
7
+ }
Binary file
@@ -10,26 +10,25 @@ public class ReactNativePasskeyAutofillModule: Module {
10
10
  // The module will be accessible from `requireNativeModule('ReactNativePasskeyAutofill')` in JavaScript.
11
11
  Name("ReactNativePasskeyAutofill")
12
12
 
13
- // Defines constant property on the module.
14
- Constant("PI") {
15
- Double.pi
13
+ AsyncFunction("setMasterKey") { (secret: String) in
14
+ // TODO: Implement for iOS
16
15
  }
17
16
 
18
- // Defines event names that the module can send to JavaScript.
19
- Events("onChange")
17
+ AsyncFunction("setHdRootKeyId") { (id: String) in
18
+ // TODO: Implement for iOS
19
+ }
20
+
21
+ AsyncFunction("getHdRootKeyId") { () -> String? in
22
+ // TODO: Implement for iOS
23
+ return nil
24
+ }
20
25
 
21
- // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
22
- Function("hello") {
23
- return "Hello world! ๐Ÿ‘‹"
26
+ AsyncFunction("clearCredentials") {
27
+ // TODO: Implement for iOS
24
28
  }
25
29
 
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
- ])
30
+ AsyncFunction("configureIntentActions") { (getPasskeyAction: String, createPasskeyAction: String) in
31
+ // TODO: Implement for iOS
33
32
  }
34
33
 
35
34
  // 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,13 @@
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: npm run lint
8
+
9
+ pre-push:
10
+ parallel: true
11
+ commands:
12
+ test:
13
+ run: npm run test:all
package/package.json CHANGED
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "name": "@algorandfoundation/react-native-passkey-autofill",
3
- "version": "1.0.0-canary.4",
3
+ "version": "1.0.0-canary.6",
4
4
  "description": "Passkey AutoFill React Native",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "publishConfig": {
8
- "access": "public"
8
+ "access": "public",
9
+ "provenance": true
9
10
  },
10
11
  "scripts": {
11
12
  "build": "expo-module build",
12
13
  "clean": "expo-module clean",
13
- "lint": "expo-module lint",
14
- "test": "expo-module test",
15
- "prepare": "expo-module prepare",
14
+ "lint": "ESLINT_USE_FLAT_CONFIG=false expo-module lint",
15
+ "test": "CI=true expo-module test",
16
+ "test:android": "cd example/android && ./gradlew :algorandfoundation-react-native-passkey-autofill:testDebugUnitTest --no-daemon",
17
+ "test:android:instrumented": "cd example/android && ./gradlew :algorandfoundation-react-native-passkey-autofill:connectedDebugAndroidTest --no-daemon",
18
+ "test:all": "npm run test && npm run test:android",
19
+ "prepare": "expo-module prepare && npx lefthook install",
16
20
  "prepublishOnly": "expo-module prepublishOnly",
17
21
  "expo-module": "expo-module",
18
22
  "open:ios": "xed example/ios",
@@ -24,22 +28,38 @@
24
28
  "@algorandfoundation/react-native-passkey-autofill",
25
29
  "ReactNativePasskeyAutofill"
26
30
  ],
27
- "repository": "https://github.com/algorandfoundation/react-native-passkey-autofill",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+ssh://git@github.com/algorandfoundation/react-native-passkey-autofill.git"
34
+ },
28
35
  "bugs": {
29
36
  "url": "https://github.com/algorandfoundation/react-native-passkey-autofill/issues"
30
37
  },
31
- "author": "Algorand Foundation <michael.feher@algorand.foundation> (https://github.com/algorandfoundation)",
38
+ "author": "Algorand Foundation",
32
39
  "license": "Apache-2.0",
33
40
  "homepage": "https://github.com/algorandfoundation/react-native-passkey-autofill#readme",
34
41
  "dependencies": {
35
42
  "@algorandfoundation/react-native-keystore": "^1.0.0-canary.6"
36
43
  },
44
+ "jest": {
45
+ "preset": "ts-jest",
46
+ "testEnvironment": "node",
47
+ "testMatch": [
48
+ "<rootDir>/src/**/__tests__/**/*.test.ts"
49
+ ]
50
+ },
37
51
  "devDependencies": {
38
52
  "@expo/config-plugins": "^9.0.0",
53
+ "@semantic-release/changelog": "^6.0.3",
54
+ "@semantic-release/git": "^10.0.1",
55
+ "@semantic-release/github": "^12.0.6",
56
+ "@semantic-release/npm": "^13.1.5",
39
57
  "@types/react": "~19.1.1",
40
58
  "expo": "^55.0.1",
41
59
  "expo-module-scripts": "^55.0.2",
42
- "react-native": "0.82.1"
60
+ "lefthook": "^2.1.4",
61
+ "react-native": "0.82.1",
62
+ "semantic-release": "^25.0.3"
43
63
  },
44
64
  "peerDependencies": {
45
65
  "expo": "*",