From acd89885825bbd3a7565448f3109b267f35fbf54 Mon Sep 17 00:00:00 2001 From: Jens Verbeken Date: Mon, 15 Jun 2026 11:55:33 +0200 Subject: [PATCH 1/4] feat(android): support Android 15 Credential Manager customizations --- android/build.gradle | 4 +- android/gradle.properties | 4 +- .../com/reactnativepasskey/PasskeyModule.kt | 67 ++++++++++++++++--- src/Passkey.ts | 43 ++++++++---- src/PasskeyTypes.ts | 15 +++++ src/__tests__/PasskeyAndroid.spec.ts | 48 ++++++++++++- src/__tests__/PasskeyiOS.spec.ts | 3 +- src/index.ts | 4 ++ 8 files changed, 160 insertions(+), 28 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 28701c6..a0aeb87 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -132,8 +132,8 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" // Android Credentials - implementation "androidx.credentials:credentials-play-services-auth:1.5.0" - implementation "androidx.credentials:credentials:1.5.0" + implementation "androidx.credentials:credentials-play-services-auth:1.6.0" + implementation "androidx.credentials:credentials:1.6.0" // Kotlin Coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2' diff --git a/android/gradle.properties b/android/gradle.properties index f4255dd..32ae029 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ Passkey_kotlinVersion=1.8.0 Passkey_minSdkVersion=21 -Passkey_targetSdkVersion=31 -Passkey_compileSdkVersion=31 +Passkey_targetSdkVersion=35 +Passkey_compileSdkVersion=35 Passkey_ndkversion=21.4.7075529 diff --git a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt index d1f2adb..842325c 100644 --- a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt +++ b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt @@ -4,6 +4,8 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableMap +import android.os.Build import androidx.credentials.CredentialManager import androidx.credentials.CreatePublicKeyCredentialRequest @@ -26,9 +28,22 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav } @ReactMethod - fun create(requestJson: String, forcePlatformKey: Boolean, forceSecurityKey: Boolean, promise: Promise) { + fun create(requestJson: String, forcePlatformKey: Boolean, forceSecurityKey: Boolean, options: ReadableMap?, promise: Promise) { val credentialManager = CredentialManager.create(reactApplicationContext.applicationContext) - val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(requestJson) + + val parsedOptions = parseCustomizationOptions(options) + + val createPublicKeyCredentialRequest = if (Build.VERSION.SDK_INT >= 35) { + CreatePublicKeyCredentialRequest( + requestJson = requestJson, + clientDataHash = null, + preferImmediatelyAvailableCredentials = parsedOptions.preferImmediatelyAvailable, + origin = parsedOptions.origin, + isAutoSelectAllowed = parsedOptions.autoSelectAllowed + ) + } else { + CreatePublicKeyCredentialRequest(requestJson) + } mainScope.launch { try { @@ -76,13 +91,27 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav } @ReactMethod - fun get(requestJson: String, forcePlatformKey: Boolean, forceSecurityKey: Boolean, preferImmediatelyAvailable: Boolean, promise: Promise) { + fun get(requestJson: String, forcePlatformKey: Boolean, forceSecurityKey: Boolean, preferImmediatelyAvailable: Boolean, options: ReadableMap?, promise: Promise) { val credentialManager = CredentialManager.create(reactApplicationContext.applicationContext) - val getCredentialRequest = - GetCredentialRequest( - listOf(GetPublicKeyCredentialOption(requestJson)), - preferImmediatelyAvailableCredentials = preferImmediatelyAvailable - ) + + val parsedOptions = parseCustomizationOptions(options) + val finalPreferImmediatelyAvailable = preferImmediatelyAvailable || parsedOptions.preferImmediatelyAvailable + + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(requestJson) + if (parsedOptions.autoSelectAllowed) { + getPublicKeyCredentialOption.requestData.putBoolean("androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED", true) + getPublicKeyCredentialOption.candidateQueryData.putBoolean("androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED", true) + } + + val getCredentialRequestBuilder = GetCredentialRequest.Builder() + .addCredentialOption(getPublicKeyCredentialOption) + .setPreferImmediatelyAvailableCredentials(finalPreferImmediatelyAvailable) + + if (Build.VERSION.SDK_INT >= 35 && parsedOptions.origin != null) { + getCredentialRequestBuilder.setOrigin(parsedOptions.origin!!) + } + + val getCredentialRequest = getCredentialRequestBuilder.build() mainScope.launch { try { @@ -145,4 +174,26 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav else -> "UnknownError" } } + + private fun parseCustomizationOptions(options: ReadableMap?): CustomizationOptions { + val result = CustomizationOptions() + if (options == null) return result + + if (options.hasKey("autoSelectAllowed")) { + result.autoSelectAllowed = options.getBoolean("autoSelectAllowed") + } + if (options.hasKey("preferImmediatelyAvailable")) { + result.preferImmediatelyAvailable = options.getBoolean("preferImmediatelyAvailable") + } + if (options.hasKey("origin")) { + result.origin = options.getString("origin") + } + return result + } + + class CustomizationOptions { + var autoSelectAllowed: Boolean = false + var preferImmediatelyAvailable: Boolean = false + var origin: String? = null + } } diff --git a/src/Passkey.ts b/src/Passkey.ts index bce4d24..77bec7d 100644 --- a/src/Passkey.ts +++ b/src/Passkey.ts @@ -9,6 +9,7 @@ import type { PasskeyCreateResult, PasskeyGetRequest, PasskeyGetResult, + PasskeyOptions, } from './PasskeyTypes'; import { stringifyPasskeyRequest } from './PasskeyRequest'; import { NativePasskey } from './NativePasskey'; @@ -23,7 +24,8 @@ export class Passkey { * @throws */ public static async create( - request: PasskeyCreateRequest + request: PasskeyCreateRequest, + options?: PasskeyOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -33,7 +35,8 @@ export class Passkey { const response = await NativePasskey.create( stringifyPasskeyRequest(request, Platform.OS), false, // forcePlatformKey - false // forceSecurityKey + false, // forceSecurityKey + options?.androidOptions ?? null ); if (typeof response === 'string') { @@ -55,7 +58,8 @@ export class Passkey { * @throws */ public static async createPlatformKey( - request: PasskeyCreateRequest + request: PasskeyCreateRequest, + options?: PasskeyOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -65,7 +69,8 @@ export class Passkey { const response = await NativePasskey.create( stringifyPasskeyRequest(request, Platform.OS), true, // forcePlatformKey - false // forceSecurityKey + false, // forceSecurityKey + options?.androidOptions ?? null ); if (typeof response === 'string') { @@ -87,7 +92,8 @@ export class Passkey { * @throws */ public static async createSecurityKey( - request: PasskeyCreateRequest + request: PasskeyCreateRequest, + options?: PasskeyOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -97,7 +103,8 @@ export class Passkey { const response = await NativePasskey.create( stringifyPasskeyRequest(request, Platform.OS), false, // forcePlatformKey - true // forceSecurityKey + true, // forceSecurityKey + options?.androidOptions ?? null ); if (typeof response === 'string') { @@ -118,7 +125,8 @@ export class Passkey { * @throws */ public static async get( - request: PasskeyGetRequest + request: PasskeyGetRequest, + options?: PasskeyOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -129,7 +137,8 @@ export class Passkey { stringifyPasskeyRequest(request, Platform.OS), false, // forcePlatformKey false, // forceSecurityKey - false // preferImmediatelyAvailable + false, // preferImmediatelyAvailable + options?.androidOptions ?? null ); if (typeof response === 'string') { @@ -155,7 +164,8 @@ export class Passkey { * @throws */ public static async getImmediate( - request: PasskeyGetRequest + request: PasskeyGetRequest, + options?: PasskeyOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -166,7 +176,8 @@ export class Passkey { stringifyPasskeyRequest(request, Platform.OS), true, // forcePlatformKey (immediate is platform-only) false, // forceSecurityKey - true // preferImmediatelyAvailable + true, // preferImmediatelyAvailable + options?.androidOptions ?? null ); if (typeof response === 'string') { @@ -188,7 +199,8 @@ export class Passkey { * @throws */ public static async getPlatformKey( - request: PasskeyGetRequest + request: PasskeyGetRequest, + options?: PasskeyOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -199,7 +211,8 @@ export class Passkey { stringifyPasskeyRequest(request, Platform.OS), true, // forcePlatformKey false, // forceSecurityKey - false // preferImmediatelyAvailable + false, // preferImmediatelyAvailable + options?.androidOptions ?? null ); if (typeof response === 'string') { @@ -221,7 +234,8 @@ export class Passkey { * @throws */ public static async getSecurityKey( - request: PasskeyGetRequest + request: PasskeyGetRequest, + options?: PasskeyOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -232,7 +246,8 @@ export class Passkey { stringifyPasskeyRequest(request, Platform.OS), false, // forcePlatformKey true, // forceSecurityKey - false // preferImmediatelyAvailable + false, // preferImmediatelyAvailable + options?.androidOptions ?? null ); if (typeof response === 'string') { diff --git a/src/PasskeyTypes.ts b/src/PasskeyTypes.ts index a5f4e00..d9c3aee 100644 --- a/src/PasskeyTypes.ts +++ b/src/PasskeyTypes.ts @@ -146,3 +146,18 @@ export interface AuthenticationExtensionsPRFValues { first: PasskeyBinaryValue; second?: PasskeyBinaryValue; } + +export interface Android15CustomizationOptions { + autoSelectAllowed?: boolean; + preferImmediatelyAvailable?: boolean; + themeVariant?: 'system' | 'light' | 'dark'; + displayHint?: { + title?: string; + subtitle?: string; + }; + origin?: string; +} + +export interface PasskeyOptions { + androidOptions?: Android15CustomizationOptions; +} diff --git a/src/__tests__/PasskeyAndroid.spec.ts b/src/__tests__/PasskeyAndroid.spec.ts index 9c2d6ba..8fac310 100644 --- a/src/__tests__/PasskeyAndroid.spec.ts +++ b/src/__tests__/PasskeyAndroid.spec.ts @@ -92,7 +92,53 @@ describe('Test Passkey Module', () => { stringifyPasskeyRequest(AuthRequest, 'android'), true, false, - true + true, + null + ); + }); + + test('should call native register method with options', async () => { + const registerSpy = jest + .spyOn(NativeModules.Passkey, 'create') + .mockResolvedValue(JSON.stringify(RegAndroidResult)); + + const options = { + androidOptions: { + autoSelectAllowed: true, + preferImmediatelyAvailable: true, + origin: 'https://example.com', + }, + }; + + await Passkey.create(RegRequest, options); + expect(registerSpy).toHaveBeenCalledWith( + stringifyPasskeyRequest(RegRequest, 'android'), + false, + false, + options.androidOptions + ); + }); + + test('should call native auth method with options', async () => { + const authSpy = jest + .spyOn(NativeModules.Passkey, 'get') + .mockResolvedValue(JSON.stringify(AuthAndroidResult)); + + const options = { + androidOptions: { + autoSelectAllowed: true, + preferImmediatelyAvailable: true, + origin: 'https://example.com', + }, + }; + + await Passkey.get(AuthRequest, options); + expect(authSpy).toHaveBeenCalledWith( + stringifyPasskeyRequest(AuthRequest, 'android'), + false, + false, + false, + options.androidOptions ); }); }); diff --git a/src/__tests__/PasskeyiOS.spec.ts b/src/__tests__/PasskeyiOS.spec.ts index 2c74a4f..c006865 100644 --- a/src/__tests__/PasskeyiOS.spec.ts +++ b/src/__tests__/PasskeyiOS.spec.ts @@ -53,7 +53,8 @@ describe('Test Passkey Module', () => { stringifyPasskeyRequest(AuthRequest, 'ios'), true, false, - true + true, + null ); }); }); diff --git a/src/index.ts b/src/index.ts index df2b895..63e9300 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import type { PasskeyCreateResult, PasskeyGetRequest, PasskeyGetResult, + Android15CustomizationOptions, + PasskeyOptions, } from './PasskeyTypes'; export { @@ -14,4 +16,6 @@ export { PasskeyCreateResult, PasskeyGetRequest, PasskeyGetResult, + Android15CustomizationOptions, + PasskeyOptions, }; From a7c84dadb819e8408dd85439f777cc032d2a1b2b Mon Sep 17 00:00:00 2001 From: Jens Verbeken Date: Mon, 15 Jun 2026 13:13:26 +0200 Subject: [PATCH 2/4] feat(android): support Android 15 Credential Manager customizations --- .../src/main/java/com/reactnativepasskey/PasskeyModule.kt | 7 ++++++- src/PasskeyTypes.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt index 842325c..ea36008 100644 --- a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt +++ b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt @@ -39,7 +39,8 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav clientDataHash = null, preferImmediatelyAvailableCredentials = parsedOptions.preferImmediatelyAvailable, origin = parsedOptions.origin, - isAutoSelectAllowed = parsedOptions.autoSelectAllowed + isAutoSelectAllowed = parsedOptions.autoSelectAllowed, + isConditional = parsedOptions.isConditional ) } else { CreatePublicKeyCredentialRequest(requestJson) @@ -188,6 +189,9 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav if (options.hasKey("origin")) { result.origin = options.getString("origin") } + if (options.hasKey("isConditional")) { + result.isConditional = options.getBoolean("isConditional") + } return result } @@ -195,5 +199,6 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav var autoSelectAllowed: Boolean = false var preferImmediatelyAvailable: Boolean = false var origin: String? = null + var isConditional: Boolean = false } } diff --git a/src/PasskeyTypes.ts b/src/PasskeyTypes.ts index d9c3aee..47ab5b3 100644 --- a/src/PasskeyTypes.ts +++ b/src/PasskeyTypes.ts @@ -156,6 +156,7 @@ export interface Android15CustomizationOptions { subtitle?: string; }; origin?: string; + isConditional?: boolean; } export interface PasskeyOptions { From 081496763fdf7f98d0c6d8f6aa70b3a6cce8e2a8 Mon Sep 17 00:00:00 2001 From: Jens Verbeken Date: Mon, 15 Jun 2026 16:27:50 +0200 Subject: [PATCH 3/4] Undo --- .../com/reactnativepasskey/PasskeyModule.kt | 10 ++-------- src/Passkey.ts | 17 +++++++++-------- src/PasskeyTypes.ts | 18 ++++++++++++++---- src/__tests__/PasskeyAndroid.spec.ts | 2 -- src/index.ts | 14 ++++++++++---- 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt index ea36008..4710ce0 100644 --- a/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt +++ b/android/src/main/java/com/reactnativepasskey/PasskeyModule.kt @@ -38,7 +38,7 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav requestJson = requestJson, clientDataHash = null, preferImmediatelyAvailableCredentials = parsedOptions.preferImmediatelyAvailable, - origin = parsedOptions.origin, + origin = null, isAutoSelectAllowed = parsedOptions.autoSelectAllowed, isConditional = parsedOptions.isConditional ) @@ -108,9 +108,7 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav .addCredentialOption(getPublicKeyCredentialOption) .setPreferImmediatelyAvailableCredentials(finalPreferImmediatelyAvailable) - if (Build.VERSION.SDK_INT >= 35 && parsedOptions.origin != null) { - getCredentialRequestBuilder.setOrigin(parsedOptions.origin!!) - } + val getCredentialRequest = getCredentialRequestBuilder.build() @@ -186,9 +184,6 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav if (options.hasKey("preferImmediatelyAvailable")) { result.preferImmediatelyAvailable = options.getBoolean("preferImmediatelyAvailable") } - if (options.hasKey("origin")) { - result.origin = options.getString("origin") - } if (options.hasKey("isConditional")) { result.isConditional = options.getBoolean("isConditional") } @@ -198,7 +193,6 @@ class PasskeyModule(reactContext: ReactApplicationContext) : ReactContextBaseJav class CustomizationOptions { var autoSelectAllowed: Boolean = false var preferImmediatelyAvailable: Boolean = false - var origin: String? = null var isConditional: Boolean = false } } diff --git a/src/Passkey.ts b/src/Passkey.ts index 77bec7d..6538d34 100644 --- a/src/Passkey.ts +++ b/src/Passkey.ts @@ -9,7 +9,8 @@ import type { PasskeyCreateResult, PasskeyGetRequest, PasskeyGetResult, - PasskeyOptions, + PasskeyCreateOptions, + PasskeyGetOptions, } from './PasskeyTypes'; import { stringifyPasskeyRequest } from './PasskeyRequest'; import { NativePasskey } from './NativePasskey'; @@ -25,7 +26,7 @@ export class Passkey { */ public static async create( request: PasskeyCreateRequest, - options?: PasskeyOptions + options?: PasskeyCreateOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -59,7 +60,7 @@ export class Passkey { */ public static async createPlatformKey( request: PasskeyCreateRequest, - options?: PasskeyOptions + options?: PasskeyCreateOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -93,7 +94,7 @@ export class Passkey { */ public static async createSecurityKey( request: PasskeyCreateRequest, - options?: PasskeyOptions + options?: PasskeyCreateOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -126,7 +127,7 @@ export class Passkey { */ public static async get( request: PasskeyGetRequest, - options?: PasskeyOptions + options?: PasskeyGetOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -165,7 +166,7 @@ export class Passkey { */ public static async getImmediate( request: PasskeyGetRequest, - options?: PasskeyOptions + options?: PasskeyGetOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -200,7 +201,7 @@ export class Passkey { */ public static async getPlatformKey( request: PasskeyGetRequest, - options?: PasskeyOptions + options?: PasskeyGetOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; @@ -235,7 +236,7 @@ export class Passkey { */ public static async getSecurityKey( request: PasskeyGetRequest, - options?: PasskeyOptions + options?: PasskeyGetOptions ): Promise { if (!Passkey.isSupported()) { throw NotSupportedError; diff --git a/src/PasskeyTypes.ts b/src/PasskeyTypes.ts index 47ab5b3..95f2ea6 100644 --- a/src/PasskeyTypes.ts +++ b/src/PasskeyTypes.ts @@ -147,7 +147,7 @@ export interface AuthenticationExtensionsPRFValues { second?: PasskeyBinaryValue; } -export interface Android15CustomizationOptions { +export interface BaseAndroid15CustomizationOptions { autoSelectAllowed?: boolean; preferImmediatelyAvailable?: boolean; themeVariant?: 'system' | 'light' | 'dark'; @@ -155,10 +155,20 @@ export interface Android15CustomizationOptions { title?: string; subtitle?: string; }; - origin?: string; +} + +export interface Android15CreateCustomizationOptions + extends BaseAndroid15CustomizationOptions { isConditional?: boolean; } -export interface PasskeyOptions { - androidOptions?: Android15CustomizationOptions; +export interface PasskeyCreateOptions { + androidOptions?: Android15CreateCustomizationOptions; +} + +export interface Android15GetCustomizationOptions + extends BaseAndroid15CustomizationOptions {} + +export interface PasskeyGetOptions { + androidOptions?: Android15GetCustomizationOptions; } diff --git a/src/__tests__/PasskeyAndroid.spec.ts b/src/__tests__/PasskeyAndroid.spec.ts index 8fac310..52eac27 100644 --- a/src/__tests__/PasskeyAndroid.spec.ts +++ b/src/__tests__/PasskeyAndroid.spec.ts @@ -106,7 +106,6 @@ describe('Test Passkey Module', () => { androidOptions: { autoSelectAllowed: true, preferImmediatelyAvailable: true, - origin: 'https://example.com', }, }; @@ -128,7 +127,6 @@ describe('Test Passkey Module', () => { androidOptions: { autoSelectAllowed: true, preferImmediatelyAvailable: true, - origin: 'https://example.com', }, }; diff --git a/src/index.ts b/src/index.ts index 63e9300..41e5e79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,11 @@ import type { PasskeyCreateResult, PasskeyGetRequest, PasskeyGetResult, - Android15CustomizationOptions, - PasskeyOptions, + Android15CreateCustomizationOptions, + PasskeyCreateOptions, + Android15GetCustomizationOptions, + PasskeyGetOptions, + BaseAndroid15CustomizationOptions, } from './PasskeyTypes'; export { @@ -16,6 +19,9 @@ export { PasskeyCreateResult, PasskeyGetRequest, PasskeyGetResult, - Android15CustomizationOptions, - PasskeyOptions, + Android15CreateCustomizationOptions, + PasskeyCreateOptions, + Android15GetCustomizationOptions, + PasskeyGetOptions, + BaseAndroid15CustomizationOptions, }; From eb0526c068304cf010269a68451386f03270fe87 Mon Sep 17 00:00:00 2001 From: Jens Verbeken Date: Tue, 16 Jun 2026 09:51:25 +0200 Subject: [PATCH 4/4] update types --- src/PasskeyTypes.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/PasskeyTypes.ts b/src/PasskeyTypes.ts index 95f2ea6..03e5f3c 100644 --- a/src/PasskeyTypes.ts +++ b/src/PasskeyTypes.ts @@ -148,7 +148,19 @@ export interface AuthenticationExtensionsPRFValues { } export interface BaseAndroid15CustomizationOptions { + /** + * If true, allows the system to automatically select a credential without + * prompting the user with a dialog, provided there is exactly one matching credential. + */ autoSelectAllowed?: boolean; + /** + * If true, specifies a preference for credentials that are immediately available on the device + * (e.g. local biometrics) rather than initiating a flow that requires external devices + * (like security keys or another phone via QR code). + * + * If no local credentials are immediately available, the operation will fail silently + * with a 'NoCredentials' error. + */ preferImmediatelyAvailable?: boolean; themeVariant?: 'system' | 'light' | 'dark'; displayHint?: { @@ -159,6 +171,15 @@ export interface BaseAndroid15CustomizationOptions { export interface Android15CreateCustomizationOptions extends BaseAndroid15CustomizationOptions { + /** + * If true, enables silent passkey creation (conditional registration). The system + * attempts to create the passkey in the background without immediately showing a popup dialog. + * + * NOTE: This requires that the user already has a saved password credential for the same + * account in their password manager (e.g., Google Password Manager). If this condition is not met, + * the call will fail with a 'NoCreateOption' error, and the app should fall back to calling + * `Passkey.create` with `isConditional: false` (an interactive prompt). + */ isConditional?: boolean; }