@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
@@ -24,10 +24,17 @@ import androidx.credentials.provider.CreateEntry
24
24
  import androidx.credentials.provider.CredentialProviderService
25
25
  import androidx.credentials.provider.ProviderClearCredentialStateRequest
26
26
  import androidx.credentials.provider.PublicKeyCredentialEntry
27
+ import androidx.credentials.provider.BiometricPromptData
28
+ import androidx.biometric.BiometricPrompt
29
+ import co.algorand.passkeyautofill.auth.BiometricRequirement
27
30
  import co.algorand.passkeyautofill.credentials.CredentialRepository
28
31
  import co.algorand.passkeyautofill.credentials.Credential
32
+ import co.algorand.passkeyautofill.utils.PasskeyUtils
33
+ import android.util.Base64 as AndroidBase64
29
34
  import com.tencent.mmkv.MMKV
35
+ import javax.crypto.Cipher
30
36
  import kotlinx.coroutines.*
37
+ import org.json.JSONArray
31
38
  import org.json.JSONObject
32
39
 
33
40
  @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -43,6 +50,25 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
43
50
  const val CREATE_PASSKEY_INTENT = 2
44
51
  const val DEFAULT_GET_PASSKEY_ACTION = "co.algorand.passkeyautofill.GET_PASSKEY"
45
52
  const val DEFAULT_CREATE_PASSKEY_ACTION = "co.algorand.passkeyautofill.CREATE_PASSKEY"
53
+
54
+ /**
55
+ * MMKV key holding the last time Android routed a credential request to
56
+ * this service. Set from both `onBeginCreate` and `onBeginGet` callbacks.
57
+ * Consumed by `ReactNativePasskeyAutofillModule.isProviderActive` as a
58
+ * reliable "this app is a registered provider" signal, since the raw
59
+ * `Settings.Secure("credential_service")` key is @hide-restricted on
60
+ * Android 12+ and not readable from regular apps.
61
+ */
62
+ const val KEY_LAST_INVOKED_AT_MS = "provider_last_invoked_at_ms"
63
+
64
+ internal fun stampActivated(context: android.content.Context) {
65
+ try {
66
+ MMKV.initialize(context)
67
+ MMKV.defaultMMKV()?.encode(KEY_LAST_INVOKED_AT_MS, System.currentTimeMillis())
68
+ } catch (e: Exception) {
69
+ Log.w(TAG, "Failed to stamp provider activation: ${e.message}")
70
+ }
71
+ }
46
72
  }
47
73
  /**
48
74
  * Handle Create Credential Requests
@@ -53,6 +79,7 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
53
79
  cancellationSignal: CancellationSignal,
54
80
  callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>
55
81
  ) {
82
+ stampActivated(this)
56
83
  val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
57
84
  if (response != null) {
58
85
  callback.onResult(response)
@@ -89,21 +116,43 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
89
116
  return BeginCreateCredentialResponse(createEntries)
90
117
  }
91
118
 
92
- val userJson = JSONObject(request.requestJson).optJSONObject("user")
119
+ val pkJson = JSONObject(request.requestJson)
120
+ val pk = if (pkJson.has("publicKey")) pkJson.getJSONObject("publicKey") else pkJson
121
+ val userJson = pk.optJSONObject("user")
93
122
  val name = userJson?.optString("name") ?: "New Passkey"
123
+
124
+ val authenticatorSelection = pk.optJSONObject("authenticatorSelection")
125
+ val userVerification = authenticatorSelection?.optString("userVerification") ?: "preferred"
126
+ Log.d(TAG, "handleCreatePasskeyQuery: userVerification=$userVerification")
94
127
 
95
128
  val action = credentialRepository.getCreatePasskeyAction(this) ?: DEFAULT_CREATE_PASSKEY_ACTION
96
129
 
97
130
  val data = Bundle()
98
131
  data.putString("requestJson", request.requestJson)
132
+ data.putString("userVerification", userVerification)
99
133
 
100
- val createEntry = CreateEntry.Builder(
134
+ val builder = CreateEntry.Builder(
101
135
  name,
102
136
  createNewPendingIntent(action, CREATE_PASSKEY_INTENT, data)
103
137
  ).setIcon(Icon.createWithResource(this, android.R.drawable.ic_menu_add))
104
- .build()
138
+
139
+ if (userVerification != "discouraged") {
140
+ try {
141
+ val requirement = BiometricRequirement.resolve(this)
142
+ val biometricPromptDataBuilder = BiometricPromptData.Builder()
143
+ .setAllowedAuthenticators(requirement.allowedAuthenticators)
144
+ if (requirement.isCryptoBound) {
145
+ val cipher = credentialRepository.getBiometricCipherForEncryption(this, requirement)
146
+ biometricPromptDataBuilder.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
147
+ }
148
+ builder.setBiometricPromptData(biometricPromptDataBuilder.build())
149
+ Log.d(TAG, "Set BiometricPromptData for CreateEntry")
150
+ } catch (e: Exception) {
151
+ Log.e(TAG, "Failed to set BiometricPromptData for CreateEntry", e)
152
+ }
153
+ }
105
154
 
106
- createEntries.add(createEntry)
155
+ createEntries.add(builder.build())
107
156
  return BeginCreateCredentialResponse(createEntries)
108
157
  }
109
158
  /**
@@ -115,6 +164,7 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
115
164
  cancellationSignal: CancellationSignal,
116
165
  callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
117
166
  ) {
167
+ stampActivated(this)
118
168
  try {
119
169
  val response = processGetCredentialRequest(request)
120
170
  callback.onResult(response)
@@ -127,7 +177,7 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
127
177
  /**
128
178
  * Fake a list of available PublicKeyCredential Entries
129
179
  */
130
- private fun processGetCredentialRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse{
180
+ private fun processGetCredentialRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse {
131
181
  // Ensure we have a master key available. If not, we can't sign anything, so don't show entries.
132
182
  if (!credentialRepository.isMasterKeyAvailable(this)) {
133
183
  return BeginGetCredentialResponse(emptyList())
@@ -138,42 +188,101 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
138
188
  }
139
189
 
140
190
  val action = credentialRepository.getGetPasskeyAction(this) ?: DEFAULT_GET_PASSKEY_ACTION
191
+ val allEntries = mutableListOf<PublicKeyCredentialEntry>()
141
192
 
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
- }
193
+ for (option in request.beginGetCredentialOptions) {
194
+ if (option !is BeginGetPublicKeyCredentialOption) continue
156
195
 
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()
196
+ val requestJsonStr = option.requestJson
197
+ var allowCredentials: JSONArray? = null
198
+ var userVerification = "preferred"
199
+ try {
200
+ val json = JSONObject(requestJsonStr)
201
+ val pk = if (json.has("publicKey")) json.getJSONObject("publicKey") else json
202
+ allowCredentials = pk.optJSONArray("allowCredentials")
203
+ userVerification = pk.optString("userVerification", "preferred")
204
+ Log.d(TAG, "handleGetPasskeyQuery: userVerification=$userVerification")
170
205
  } catch (e: Exception) {
171
- Log.e(TAG, "Error building PublicKeyCredentialEntry", e)
206
+ Log.e(TAG, "Error parsing requestJson", e)
207
+ }
208
+
209
+ val allowedIds = if (allowCredentials != null && allowCredentials.length() > 0) {
210
+ val ids = mutableSetOf<String>()
211
+ for (i in 0 until allowCredentials.length()) {
212
+ ids.add(allowCredentials.getJSONObject(i).getString("id"))
213
+ }
214
+ ids
215
+ } else {
172
216
  null
173
217
  }
218
+
219
+ for (credential in credentials) {
220
+ if (allowedIds != null) {
221
+ val isAllowed = allowedIds.any { allowedId ->
222
+ allowedId == credential.credentialId ||
223
+ PasskeyUtils.normalizeBase64(allowedId) == PasskeyUtils.normalizeBase64(credential.credentialId)
224
+ }
225
+ if (!isAllowed) continue
226
+ }
227
+
228
+ try {
229
+ val data = Bundle()
230
+ data.putString("credentialId", credential.credentialId)
231
+ data.putString("userHandle", credential.userHandle)
232
+ data.putString("requestJson", option.requestJson)
233
+ data.putString("userVerification", userVerification)
234
+ credential.biometricIv?.let { data.putString("biometricIv", it) }
235
+
236
+ // Use a unique requestCode for each PendingIntent to avoid data being overwritten
237
+ val uniqueRequestCode = (credential.credentialId + option.requestJson).hashCode()
238
+
239
+ val entryBuilder = PublicKeyCredentialEntry.Builder(
240
+ this,
241
+ credential.userHandle,
242
+ createNewPendingIntent(action, uniqueRequestCode, data),
243
+ option
244
+ ).setIcon(Icon.createWithResource(this, android.R.drawable.ic_lock_lock))
245
+
246
+ if (userVerification != "discouraged") {
247
+ try {
248
+ val requirement = BiometricRequirement.resolve(this)
249
+ val biometricPromptDataBuilder = BiometricPromptData.Builder()
250
+ .setAllowedAuthenticators(requirement.allowedAuthenticators)
251
+
252
+ if (requirement.isCryptoBound) {
253
+ val iv = credential.biometricIv
254
+ if (iv != null) {
255
+ val ivBytes = AndroidBase64.decode(iv, AndroidBase64.DEFAULT)
256
+ val cipher = credentialRepository.getBiometricCipherForDecryption(this, ivBytes, requirement)
257
+ biometricPromptDataBuilder.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
258
+ } else {
259
+ // Even if the key isn't locked, provide a CryptoObject to enable Single Tap
260
+ // and ensure the user is authenticated for this operation.
261
+ try {
262
+ val cipher = credentialRepository.getBiometricCipherForEncryption(this, requirement)
263
+ biometricPromptDataBuilder.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
264
+ } catch (e: Exception) {
265
+ Log.d(TAG, "Could not get encryption cipher for Single Tap: ${e.message}")
266
+ }
267
+ }
268
+ }
269
+
270
+ entryBuilder.setBiometricPromptData(biometricPromptDataBuilder.build())
271
+ Log.d(TAG, "Set BiometricPromptData for entry ${credential.userHandle}")
272
+ } catch (e: Exception) {
273
+ Log.e(TAG, "Failed to set BiometricPromptData for entry", e)
274
+ }
275
+ }
276
+
277
+ allEntries.add(entryBuilder.build())
278
+ } catch (e: Exception) {
279
+ Log.e(TAG, "Error building PublicKeyCredentialEntry", e)
280
+ }
281
+ }
174
282
  }
175
- return BeginGetCredentialResponse(entries)
283
+ return BeginGetCredentialResponse(allEntries)
176
284
  }
285
+
177
286
  override fun onClearCredentialStateRequest(
178
287
  request: ProviderClearCredentialStateRequest,
179
288
  cancellationSignal: CancellationSignal,
@@ -186,12 +295,17 @@ class PasskeyAutofillCredentialProviderService: CredentialProviderService() {
186
295
  private fun createNewPendingIntent(action: String, requestCode: Int, extra: Bundle?): PendingIntent{
187
296
  MMKV.initialize(this)
188
297
  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")
298
+
299
+ // Determine component based on action instead of requestCode
300
+ val getAction = credentialRepository.getGetPasskeyAction(this) ?: DEFAULT_GET_PASSKEY_ACTION
301
+ val createAction = credentialRepository.getCreatePasskeyAction(this) ?: DEFAULT_CREATE_PASSKEY_ACTION
302
+
303
+ val componentName = when(action) {
304
+ getAction -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.GetPasskeyActivity")
305
+ createAction -> android.content.ComponentName(packageName, "co.algorand.passkeyautofill.CreatePasskeyActivity")
193
306
  else -> null
194
307
  }
308
+
195
309
  if (componentName != null) {
196
310
  intent.component = componentName
197
311
  } else {
@@ -0,0 +1,119 @@
1
+ package co.algorand.passkeyautofill.utils
2
+
3
+ import android.util.Log
4
+ import javax.crypto.Cipher
5
+
6
+ object PasskeyUtils {
7
+ private const val TAG = "PasskeyUtils"
8
+
9
+ fun normalizeBase64(s: String): String {
10
+ return s.replace("-", "+").replace("_", "/").replace("=", "")
11
+ }
12
+
13
+ /**
14
+ * Extracts a Cipher from a BiometricPrompt.AuthenticationResult or similar objects
15
+ * using reflection. This is necessary because the Credentials library uses
16
+ * hidden or internal wrappers for platform/androidx biometric results.
17
+ */
18
+ fun extractCipher(authResult: Any?): Cipher? {
19
+ if (authResult == null) return null
20
+ return try {
21
+ Log.i(TAG, "Attempting to extract cipher from: ${authResult.javaClass.name}")
22
+
23
+ // 1. Try direct getCryptoObject (works for androidx.biometric or platform)
24
+ var cryptoObject = authResult.javaClass.methods.find { it.name == "getCryptoObject" }?.invoke(authResult)
25
+
26
+ // 2. If not found, check if it's a wrapper like androidx.credentials.provider.AuthenticationResult
27
+ if (cryptoObject == null) {
28
+ val unwrappingMethods = listOf(
29
+ "getFrameworkAuthenticationResult",
30
+ "getBiometricPromptAuthenticationResult",
31
+ "getAuthenticationResult"
32
+ )
33
+ for (methodName in unwrappingMethods) {
34
+ try {
35
+ val method = authResult.javaClass.methods.find { it.name == methodName }
36
+ if (method != null) {
37
+ val innerResult = method.invoke(authResult)
38
+ if (innerResult != null && innerResult !== authResult) {
39
+ Log.i(TAG, "Unwrapped via $methodName to ${innerResult.javaClass.name}")
40
+ cryptoObject = innerResult.javaClass.methods.find { it.name == "getCryptoObject" }?.invoke(innerResult)
41
+ if (cryptoObject != null) break
42
+ }
43
+ }
44
+ } catch (e: Exception) { /* ignore */ }
45
+ }
46
+ }
47
+
48
+ // 2.5 Try to find hidden fields directly if methods fail
49
+ if (cryptoObject == null) {
50
+ val hiddenFields = listOf("mFrameworkAuthenticationResult", "mBiometricPromptAuthenticationResult")
51
+ for (fieldName in hiddenFields) {
52
+ try {
53
+ val field = authResult.javaClass.getDeclaredField(fieldName)
54
+ field.isAccessible = true
55
+ val innerResult = field.get(authResult)
56
+ if (innerResult != null) {
57
+ Log.i(TAG, "Unwrapped via field $fieldName to ${innerResult.javaClass.name}")
58
+ cryptoObject = innerResult.javaClass.methods.find { it.name == "getCryptoObject" }?.invoke(innerResult)
59
+ if (cryptoObject != null) break
60
+ }
61
+ } catch (e: Exception) { /* ignore */ }
62
+ }
63
+ }
64
+
65
+ // 3. Exhaustive search for any method returning something that has getCryptoObject
66
+ if (cryptoObject == null) {
67
+ for (method in authResult.javaClass.methods) {
68
+ if (method.parameterCount == 0 && method.name.startsWith("get") && method.returnType != Void.TYPE) {
69
+ try {
70
+ val candidate = method.invoke(authResult) ?: continue
71
+ if (candidate === authResult) continue
72
+
73
+ val getCrypto = candidate.javaClass.methods.find { it.name == "getCryptoObject" }
74
+ if (getCrypto != null) {
75
+ cryptoObject = getCrypto.invoke(candidate)
76
+ if (cryptoObject != null) {
77
+ Log.i(TAG, "Found cryptoObject via ${method.name} -> ${candidate.javaClass.name}")
78
+ break
79
+ }
80
+ }
81
+ } catch (e: Exception) { continue }
82
+ }
83
+ }
84
+ }
85
+
86
+ // 4. Try searching for fields too
87
+ if (cryptoObject == null) {
88
+ for (field in authResult.javaClass.declaredFields) {
89
+ try {
90
+ field.isAccessible = true
91
+ val candidate = field.get(authResult) ?: continue
92
+ val getCrypto = candidate.javaClass.methods.find { it.name == "getCryptoObject" }
93
+ if (getCrypto != null) {
94
+ cryptoObject = getCrypto.invoke(candidate)
95
+ if (cryptoObject != null) {
96
+ Log.i(TAG, "Found cryptoObject in field ${field.name}")
97
+ break
98
+ }
99
+ }
100
+ } catch (e: Exception) { continue }
101
+ }
102
+ }
103
+
104
+ if (cryptoObject == null) {
105
+ Log.w(TAG, "Could not find CryptoObject in ${authResult.javaClass.name}")
106
+ return null
107
+ }
108
+
109
+ // 5. Finally extract the Cipher from the CryptoObject
110
+ val getCipher = cryptoObject.javaClass.methods.find { it.name == "getCipher" }
111
+ val cipher = getCipher?.invoke(cryptoObject) as? Cipher
112
+ Log.i(TAG, "Extracted cipher: $cipher")
113
+ cipher
114
+ } catch (e: Exception) {
115
+ Log.w(TAG, "Failed to extract cipher via reflection", e)
116
+ null
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,10 @@
1
+ <resources>
2
+ <style name="Theme.PasskeyAutofill.Dialog" parent="Theme.AppCompat.Light.Dialog.MinWidth">
3
+ <item name="android:windowIsTranslucent">true</item>
4
+ <item name="android:windowBackground">@android:color/transparent</item>
5
+ <item name="android:windowContentOverlay">@null</item>
6
+ <item name="android:windowNoTitle">true</item>
7
+ <item name="android:windowIsFloating">true</item>
8
+ <item name="android:backgroundDimEnabled">false</item>
9
+ </style>
10
+ </resources>