@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.1 → 1.0.0-canary.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.
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", "package-lock.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,42 @@
1
+ # [1.0.0-canary.10](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.9...v1.0.0-canary.10) (2026-03-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * bouncy castle provider insertion order ([9e9039d](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/9e9039deab02f75f5a4397cedc098c0ca2e24875))
7
+
8
+ # [1.0.0-canary.9](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.8...v1.0.0-canary.9) (2026-03-30)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * x logins, refactor example views to be dialogs ([e7a1ccf](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/e7a1ccf91f88b5c369ff3082f06d538fc8b3db89))
14
+
15
+ # [1.0.0-canary.8](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.7...v1.0.0-canary.8) (2026-03-27)
16
+
17
+
18
+ ### Features
19
+
20
+ * passkey added and authenticated events ([b9871f4](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/b9871f475a9a633ae98d30a702ef2814ec8ae5e0))
21
+
22
+ # [1.0.0-canary.7](https://github.com/algorandfoundation/react-native-passkey-autofill/compare/v1.0.0-canary.6...v1.0.0-canary.7) (2026-03-24)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * update package lock and include in CI/CD ([1d775cd](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/1d775cdde170a6ef400a1ee8a01fce759ddb1f1e))
28
+
29
+ # [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)
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * include banner for package README ([60a6559](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/60a655932faac6de57daf0f30261f2d03a2c1476))
35
+
36
+ # [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)
37
+
38
+
39
+ ### Features
40
+
41
+ * **android:** add allowed credentials mapping ([e7134fc](https://github.com/algorandfoundation/react-native-passkey-autofill/commit/e7134fc1e5bf09649155e38010e4ef9728580812))
42
+ * **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,6 +1,12 @@
1
- # 🔑 react-native-passkey-autofill
1
+ # @algorandfoundation/react-native-passkey-autofill
2
2
 
3
- Passkey AutoFill for React Native using DP256.
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/algorandfoundation/react-native-passkey-autofill/refs/heads/main/assets/banner.png" width="100%" />
5
+ </p>
6
+
7
+ <p align="center">
8
+ Passkey AutoFill for React Native using DP256.
9
+ </p>
4
10
 
5
11
  # 🚀 Get Started
6
12
 
@@ -9,13 +15,36 @@ For bare React Native projects, you must ensure that you have [installed and con
9
15
  ### Add the package to your npm dependencies
10
16
 
11
17
  ```bash
12
- npm install react-native-passkey-autofill
18
+ npm install @algorandfoundation/react-native-passkey-autofill
13
19
  ```
14
20
 
15
21
  ### Configure for Android
16
22
 
17
23
  To use this module on Android, you need to configure the AutoFill service in your `AndroidManifest.xml` or via the Expo plugin.
18
24
 
25
+ #### Expo Plugin Configuration
26
+
27
+ If you are using Expo, you can configure the plugin in your `app.json` or `app.config.js`:
28
+
29
+ ```json
30
+ {
31
+ "expo": {
32
+ "plugins": [
33
+ [
34
+ "@algorandfoundation/react-native-passkey-autofill",
35
+ {
36
+ "site": "https://your-fido-server.com",
37
+ "label": "My Custom Credential Provider"
38
+ }
39
+ ]
40
+ ]
41
+ }
42
+ }
43
+ ```
44
+
45
+ - `site`: The URL of your FIDO server (default: `https://fido.shore-tech.net`).
46
+ - `label`: The name of the credential provider as it appears in Android settings (default: `My Credential Provider`).
47
+
19
48
  ## Usage
20
49
 
21
50
  ```typescript
@@ -37,17 +66,74 @@ await ReactNativePasskeyAutofill.configureIntentActions(
37
66
  await ReactNativePasskeyAutofill.clearCredentials();
38
67
  ```
39
68
 
69
+ ## Events
70
+
71
+ You can listen for events emitted by the native module when a passkey is successfully added or authenticated.
72
+
73
+ ```typescript
74
+ import ReactNativePasskeyAutofill from '@algorandfoundation/react-native-passkey-autofill';
75
+ import { useEffect } from 'react';
76
+
77
+ // ... inside a component or hook
78
+ useEffect(() => {
79
+ const addedSubscription = ReactNativePasskeyAutofill.addListener('onPasskeyAdded', (event) => {
80
+ console.log('Passkey added successfully:', event.success);
81
+ });
82
+
83
+ const authSubscription = ReactNativePasskeyAutofill.addListener('onPasskeyAuthenticated', (event) => {
84
+ console.log('Passkey authenticated successfully:', event.success);
85
+ });
86
+
87
+ return () => {
88
+ addedSubscription.remove();
89
+ authSubscription.remove();
90
+ };
91
+ }, []);
92
+ ```
93
+
94
+ ## 🧪 Testing
95
+
96
+ The project is set up with a comprehensive testing approach covering both JavaScript and Native (Kotlin) sides.
97
+
98
+ ### JavaScript Tests
99
+ Run unit tests for the TypeScript module using Jest:
100
+ ```bash
101
+ npm test
102
+ ```
103
+
104
+ ### Native Android Tests
105
+ Run unit tests for the Kotlin code using JUnit and Robolectric. These tests are executed via the example app's Gradle wrapper:
106
+ ```bash
107
+ npm run test:android
108
+ ```
109
+
110
+ ### All Tests
111
+ Run both JS and Native tests:
112
+ ```bash
113
+ npm run test:all
114
+ ```
115
+
116
+ ### Continuous Integration (CI)
117
+ 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`.
118
+
119
+ ### Integration Testing (E2E)
120
+ 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.
121
+
122
+ ## 📱 Example App
123
+
124
+ The [example](./example) app demonstrates how to integrate this module with a full wallet implementation.
125
+
40
126
  # 🤝 Contributing
41
127
 
42
128
  Contributions are very welcome! Please refer to guidelines described in the [contributing guide](./CONTRIBUTING.md).
43
129
 
44
130
  # 💖 Acknowledgements
45
131
 
46
- This has been the commination of many different efforts and ideas. We would like to thank the following individuals and organizations for their contributions:
132
+ This has been the culmination of many different efforts and ideas. We would like to thank the following individuals and organizations for their contributions:
47
133
 
48
134
  - [Bruno Martins](https://github.com/bmartins) the architect at [Algorand Foundation](https://github.com/algorandfoundation) for conceptualizing and guiding the project.
49
135
  - [HashMapsData2Value](https://github.com/HashMapsData2Value) for his guidance and support in DP256 and XHD and his work on the native autofill libraries.
50
- - [Will Beaumont](https://github.com/mjbeau) for working though integration within the Pera wallet
136
+ - [Will Beaumont](https://github.com/mjbeau) for working through integration within the Pera wallet
51
137
  - [Michael T Chuang](https://github.com/michaeltchuang) for his work in KMP integrations and client libraries.
52
138
 
53
139
  # 📄 License
@@ -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 {
@@ -5,8 +5,7 @@
5
5
  android:name=".service.PasskeyAutofillCredentialProviderService"
6
6
  android:enabled="true"
7
7
  android:exported="true"
8
- android:label="My Credential Provider"
9
- android:icon="@mipmap/ic_launcher"
8
+ android:label="@string/passkey_autofill_label"
10
9
  android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
11
10
  tools:targetApi="upside_down_cake">
12
11
  <intent-filter>
@@ -16,13 +15,19 @@
16
15
  android:name="android.credentials.provider"
17
16
  android:resource="@xml/provider" />
18
17
  </service>
19
- <activity android:name=".GetPasskeyActivity" android:exported="true">
18
+ <activity android:name="co.algorand.passkeyautofill.GetPasskeyActivity"
19
+ android:exported="true"
20
+ android:launchMode="singleTop"
21
+ android:theme="@style/Theme.PasskeyAutofill.Dialog">
20
22
  <intent-filter>
21
23
  <action android:name="co.algorand.passkeyautofill.GET_PASSKEY"/>
22
24
  <category android:name="android.intent.category.DEFAULT"/>
23
25
  </intent-filter>
24
26
  </activity>
25
- <activity android:name=".CreatePasskeyActivity" android:exported="true">
27
+ <activity android:name="co.algorand.passkeyautofill.CreatePasskeyActivity"
28
+ android:exported="true"
29
+ android:launchMode="singleTop"
30
+ android:theme="@style/Theme.PasskeyAutofill.Dialog">
26
31
  <intent-filter>
27
32
  <action android:name="co.algorand.passkeyautofill.CREATE_PASSKEY"/>
28
33
  <category android:name="android.intent.category.DEFAULT"/>
@@ -6,7 +6,12 @@ import android.content.Intent
6
6
  import android.os.Build
7
7
  import android.os.Bundle
8
8
  import android.util.Log
9
+ import android.graphics.Color
10
+ import android.graphics.Typeface
11
+ import android.view.Gravity
9
12
  import android.widget.Button
13
+ import android.widget.ImageView
14
+ import android.widget.LinearLayout
10
15
  import android.widget.TextView
11
16
  import androidx.annotation.RequiresApi
12
17
  import androidx.appcompat.app.AppCompatActivity
@@ -31,70 +36,205 @@ class CreatePasskeyActivity : AppCompatActivity() {
31
36
  const val TAG = "CreatePasskeyActivity"
32
37
  }
33
38
 
34
- private var origin: String = "unknown-origin"
39
+ private var origin: String = "unknown-origin" // Derivation origin (rpId or hash)
40
+ private var displayOrigin: String = "unknown-origin"
35
41
  private var userHandle: String = "unknown-user"
36
42
  private var userName: String = "User"
37
43
  private var userId: String = "unknown-id"
44
+ private var bundleRequestJson: String? = null
38
45
  private var request: ProviderCreateCredentialRequest? = null
39
46
 
40
47
  override fun onCreate(savedInstanceState: Bundle?) {
41
48
  super.onCreate(savedInstanceState)
42
49
 
43
50
  request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
51
+ val credentialData = intent.getBundleExtra("CREDENTIAL_DATA")
52
+
53
+ if (credentialData != null) {
54
+ bundleRequestJson = credentialData.getString("requestJson")
55
+ }
56
+
44
57
  if (request != null) {
45
58
  try {
59
+ val systemOrigin = credentialRepository.getOrigin(request!!.callingAppInfo)
60
+ origin = systemOrigin
61
+ displayOrigin = systemOrigin
62
+
46
63
  val publicKeyRequest = request!!.callingRequest as CreatePublicKeyCredentialRequest
47
- val requestJson = JSONObject(publicKeyRequest.requestJson)
64
+ val rawJson = bundleRequestJson ?: publicKeyRequest.requestJson
65
+ val requestJson = JSONObject(rawJson)
66
+ val pkJson = if (requestJson.has("publicKey")) requestJson.getJSONObject("publicKey") else requestJson
48
67
 
49
68
  // Use RP ID as origin for derivation if available, otherwise fallback to app signature
50
- val rpId = requestJson.optJSONObject("rp")?.optString("id")
51
- origin = rpId ?: credentialRepository.getOrigin(request!!.callingAppInfo)
69
+ val rpId = pkJson.optJSONObject("rp")?.optString("id")
70
+ if (rpId != null && rpId.isNotEmpty()) {
71
+ origin = rpId
72
+ displayOrigin = rpId
73
+ }
52
74
 
53
- val userJson = requestJson.optJSONObject("user")
54
- userHandle = userJson?.optString("name") ?: "unknown-user"
55
- userName = userJson?.optString("displayName") ?: userHandle
56
- userId = userJson?.optString("id") ?: "unknown-id"
75
+ userHandle = pkJson.optJSONObject("user")?.optString("name") ?: "unknown-user"
76
+ userName = pkJson.optJSONObject("user")?.optString("displayName") ?: userHandle
77
+ userId = pkJson.optJSONObject("user")?.optString("id") ?: "unknown-id"
57
78
  } catch (e: Exception) {
58
79
  Log.e(TAG, "Error parsing request", e)
59
80
  }
60
81
  }
61
82
 
62
- // Simple UI for demonstration
63
- val layout = android.widget.LinearLayout(this).apply {
64
- orientation = android.widget.LinearLayout.VERTICAL
65
- setPadding(50, 50, 50, 50)
83
+ // Improved UI
84
+ val layout = LinearLayout(this).apply {
85
+ val padding = (32 * resources.displayMetrics.density).toInt()
86
+ orientation = LinearLayout.VERTICAL
87
+ setPadding(padding, padding, padding, padding)
88
+ gravity = Gravity.CENTER_HORIZONTAL
89
+ setBackgroundColor(Color.WHITE)
66
90
  }
67
91
 
92
+ // Header with App Icon and Provider Label
93
+ try {
94
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
95
+ val appIcon = packageManager.getApplicationIcon(appInfo)
96
+ val appLabel = packageManager.getApplicationLabel(appInfo)
97
+
98
+ val density = resources.displayMetrics.density
99
+ val iconSize = (48 * density).toInt()
100
+ val iconMargin = (12 * density).toInt()
101
+ val headerPadding = (40 * density).toInt()
102
+
103
+ val header = LinearLayout(this).apply {
104
+ orientation = LinearLayout.HORIZONTAL
105
+ gravity = Gravity.CENTER_VERTICAL
106
+ setPadding(0, 0, 0, headerPadding)
107
+ }
108
+
109
+ val iconView = ImageView(this).apply {
110
+ setImageDrawable(appIcon)
111
+ layoutParams = LinearLayout.LayoutParams(iconSize, iconSize).apply {
112
+ marginEnd = iconMargin
113
+ }
114
+ }
115
+ header.addView(iconView)
116
+
117
+ val providerLabel = TextView(this).apply {
118
+ text = appLabel
119
+ textSize = 16f
120
+ typeface = Typeface.DEFAULT_BOLD
121
+ setTextColor(Color.DKGRAY)
122
+ }
123
+ header.addView(providerLabel)
124
+ layout.addView(header)
125
+ } catch (e: Exception) {
126
+ // Fallback if app info cannot be loaded
127
+ }
128
+
68
129
  val title = TextView(this).apply {
69
- text = "Create New Passkey"
70
- textSize = 24f
71
- setPadding(0, 0, 0, 50)
130
+ text = "Create Passkey"
131
+ textSize = 28f
132
+ typeface = Typeface.DEFAULT_BOLD
133
+ setPadding(0, 0, 0, (8 * resources.displayMetrics.density).toInt())
134
+ setTextColor(Color.BLACK)
72
135
  }
73
136
  layout.addView(title)
74
137
 
75
- val info = TextView(this).apply {
76
- text = "For: $userName\nAt: $origin"
77
- setPadding(0, 0, 0, 50)
138
+ val description = TextView(this).apply {
139
+ text = "Create a new passkey for $displayOrigin"
140
+ textSize = 16f
141
+ setPadding(0, 0, 0, (40 * resources.displayMetrics.density).toInt())
142
+ gravity = Gravity.CENTER_HORIZONTAL
143
+ setTextColor(Color.GRAY)
78
144
  }
79
- layout.addView(info)
145
+ layout.addView(description)
146
+
147
+ val userInfoContainer = LinearLayout(this).apply {
148
+ orientation = LinearLayout.VERTICAL
149
+ setPadding(0, 0, 0, (50 * resources.displayMetrics.density).toInt())
150
+ gravity = Gravity.CENTER_HORIZONTAL
151
+ }
152
+
153
+ val userLabel = TextView(this).apply {
154
+ text = "ACCOUNT"
155
+ textSize = 12f
156
+ typeface = Typeface.DEFAULT_BOLD
157
+ setTextColor(Color.GRAY)
158
+ setPadding(0, 0, 0, (4 * resources.displayMetrics.density).toInt())
159
+ }
160
+ userInfoContainer.addView(userLabel)
161
+
162
+ val userValue = TextView(this).apply {
163
+ text = userName
164
+ textSize = 20f
165
+ typeface = Typeface.DEFAULT_BOLD
166
+ setTextColor(Color.BLACK)
167
+ }
168
+ userInfoContainer.addView(userValue)
169
+
170
+ if (userHandle != userName) {
171
+ val handleValue = TextView(this).apply {
172
+ text = userHandle
173
+ textSize = 14f
174
+ setTextColor(Color.GRAY)
175
+ }
176
+ userInfoContainer.addView(handleValue)
177
+ }
178
+ layout.addView(userInfoContainer)
80
179
 
81
180
  val confirmButton = Button(this).apply {
82
181
  text = "Create Passkey"
83
182
  setOnClickListener {
84
183
  handleCreation()
85
184
  }
185
+ layoutParams = LinearLayout.LayoutParams(
186
+ LinearLayout.LayoutParams.MATCH_PARENT,
187
+ LinearLayout.LayoutParams.WRAP_CONTENT
188
+ ).apply {
189
+ setMargins(0, 0, 0, (8 * resources.displayMetrics.density).toInt())
190
+ }
86
191
  }
87
192
  layout.addView(confirmButton)
88
193
 
194
+ val cancelButton = Button(this, null, android.R.attr.borderlessButtonStyle).apply {
195
+ text = "Cancel"
196
+ setOnClickListener {
197
+ setResult(RESULT_CANCELED)
198
+ finish()
199
+ }
200
+ layoutParams = LinearLayout.LayoutParams(
201
+ LinearLayout.LayoutParams.MATCH_PARENT,
202
+ LinearLayout.LayoutParams.WRAP_CONTENT
203
+ )
204
+ }
205
+ layout.addView(cancelButton)
206
+
89
207
  setContentView(layout)
208
+
209
+ // Ensure the activity is focusable and can receive touches
210
+ layout.isFocusable = true
211
+ layout.isFocusableInTouchMode = true
212
+ layout.requestFocus()
90
213
  }
91
214
 
92
215
  @SuppressLint("RestrictedApi")
93
216
  private fun handleCreation() {
94
217
  try {
95
218
  val req = request ?: throw IllegalStateException("No request found")
96
- val publicKeyRequest = req.callingRequest as CreatePublicKeyCredentialRequest
97
- val requestOptions = PublicKeyCredentialCreationOptions(publicKeyRequest.requestJson)
219
+
220
+ // Prefer using the request JSON from the bundle if available
221
+ val rawRequestJson = bundleRequestJson ?: run {
222
+ val publicKeyRequest = req.callingRequest as CreatePublicKeyCredentialRequest
223
+ publicKeyRequest.requestJson
224
+ }
225
+
226
+ val requestJson = JSONObject(rawRequestJson)
227
+ val passkeyReqJson = if (requestJson.has("publicKey")) {
228
+ requestJson.getJSONObject("publicKey").toString()
229
+ } else {
230
+ rawRequestJson
231
+ }
232
+ val requestOptions = try {
233
+ PublicKeyCredentialCreationOptions(passkeyReqJson)
234
+ } catch (e: org.json.JSONException) {
235
+ Log.e(TAG, "Invalid passkey creation request JSON: $passkeyReqJson")
236
+ throw e
237
+ }
98
238
 
99
239
  val keyPair: KeyPair = credentialRepository.createDeterministicKeyPair(this, origin, userHandle)
100
240
  val credentialId = credentialRepository.generateCredentialId(keyPair)
@@ -131,25 +271,34 @@ class CreatePasskeyActivity : AppCompatActivity() {
131
271
  )
132
272
 
133
273
  val resultIntent = Intent()
134
- val responseJson = JSONObject(fidoCredential.json())
135
- val passkeyDetails = JSONObject()
136
-
137
- val ecPubKey = keyPair.public as java.security.interfaces.ECPublicKey
138
- val ecPrivKey = keyPair.private as java.security.interfaces.ECPrivateKey
139
-
140
- fun to32(bi: java.math.BigInteger): ByteArray {
141
- var ba = bi.toByteArray()
142
- if (ba.size < 32) {
143
- ba = ByteArray(32 - ba.size) + ba
274
+ val fullJson = JSONObject(fidoCredential.json())
275
+ val respJson = fullJson.getJSONObject("response")
276
+
277
+ // Ensure compact clientDataJSON for registration too
278
+ val sanitizedOrigin = credentialRepository.getOrigin(req.callingAppInfo).replace(Regex("/$"), "")
279
+ val challenge = if (requestJson.has("publicKey")) {
280
+ requestJson.getJSONObject("publicKey").getString("challenge")
281
+ } else {
282
+ requestJson.getString("challenge")
283
+ }
284
+
285
+ val clientDataJSONString = if (sanitizedOrigin.startsWith("https://") || sanitizedOrigin.startsWith("http://")) {
286
+ "{\"type\":\"webauthn.create\",\"challenge\":\"$challenge\",\"origin\":\"$sanitizedOrigin\",\"crossOrigin\":false}"
287
+ } else {
288
+ val json = JSONObject()
289
+ json.put("type", "webauthn.create")
290
+ json.put("challenge", challenge)
291
+ json.put("origin", sanitizedOrigin)
292
+ json.put("crossOrigin", false)
293
+ if (sanitizedOrigin.startsWith("android:apk-key-hash:")) {
294
+ json.put("androidPackageName", req.callingAppInfo.packageName)
144
295
  }
145
- return ba.copyOfRange(ba.size - 32, ba.size)
296
+ json.toString()
146
297
  }
147
-
148
- val x = to32(ecPubKey.w.affineX)
149
- val y = to32(ecPubKey.w.affineY)
150
- val s = to32(ecPrivKey.s)
298
+ val clientDataJSONb64 = AndroidBase64.encodeToString(clientDataJSONString.toByteArray(), AndroidBase64.URL_SAFE or AndroidBase64.NO_WRAP or AndroidBase64.NO_PADDING)
299
+ respJson.put("clientDataJSON", clientDataJSONb64)
151
300
 
152
- val createResponse = CreatePublicKeyCredentialResponse(responseJson.toString())
301
+ val createResponse = CreatePublicKeyCredentialResponse(fullJson.toString())
153
302
 
154
303
  PendingIntentHandler.setCreateCredentialResponse(
155
304
  resultIntent,
@@ -157,6 +306,9 @@ class CreatePasskeyActivity : AppCompatActivity() {
157
306
  )
158
307
 
159
308
  setResult(Activity.RESULT_OK, resultIntent)
309
+ ReactNativePasskeyAutofillModule.instance?.sendEvent("onPasskeyAdded", Bundle().apply {
310
+ putBoolean("success", true)
311
+ })
160
312
  } catch (e: Exception) {
161
313
  Log.e(TAG, "Error during passkey creation", e)
162
314
  setResult(Activity.RESULT_CANCELED)