@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 +3 -0
- package/.releaserc +23 -0
- package/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +10 -0
- package/README.md +91 -5
- package/android/build.gradle +18 -0
- package/android/src/main/AndroidManifest.xml +9 -4
- package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +189 -37
- package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +214 -38
- package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +15 -1
- package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +65 -34
- package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +7 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +10 -0
- package/app.plugin.js +7 -2
- package/assets/banner.png +0 -0
- package/build/ReactNativePasskeyAutofill.types.d.ts +8 -1
- package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
- package/ios/ReactNativePasskeyAutofillModule.swift +16 -15
- package/lefthook.yml +13 -0
- package/package.json +27 -7
- package/src/ReactNativePasskeyAutofill.types.ts +2 -1
package/.eslintrc.js
CHANGED
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
|
-
#
|
|
1
|
+
# @algorandfoundation/react-native-passkey-autofill
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
package/android/build.gradle
CHANGED
|
@@ -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="
|
|
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"
|
|
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"
|
|
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
|
|
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 =
|
|
51
|
-
|
|
69
|
+
val rpId = pkJson.optJSONObject("rp")?.optString("id")
|
|
70
|
+
if (rpId != null && rpId.isNotEmpty()) {
|
|
71
|
+
origin = rpId
|
|
72
|
+
displayOrigin = rpId
|
|
73
|
+
}
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
//
|
|
63
|
-
val layout =
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
70
|
-
textSize =
|
|
71
|
-
|
|
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
|
|
76
|
-
text = "
|
|
77
|
-
|
|
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(
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
135
|
-
val
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
val
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
296
|
+
json.toString()
|
|
146
297
|
}
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
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)
|