Skip to content

fix(android): always set Kotlin jvmTarget to 17#59

Open
christopherwxyz wants to merge 7 commits intopeterferguson:mainfrom
officialunofficial:main
Open

fix(android): always set Kotlin jvmTarget to 17#59
christopherwxyz wants to merge 7 commits intopeterferguson:mainfrom
officialunofficial:main

Conversation

@christopherwxyz
Copy link
Copy Markdown

@christopherwxyz christopherwxyz commented Feb 17, 2026

Summary

The kotlinOptions block in android/build.gradle was gated behind agpVersion < 8, so on AGP 8+ the Kotlin JVM target defaulted to whatever JDK was running Gradle. With Kotlin 2.1.20 and JDK 20, this causes a build failure:

Inconsistent JVM-target compatibility detected for tasks
'compileDebugJavaWithJavac' (17) and 'compileDebugKotlin' (20).

This PR removes the AGP version check so jvmTarget = 17 is always set, matching the compileOptions block above it.

Changes

  • Removed the if (agpVersion < 8) guard around kotlinOptions
  • Removed the duplicate compileOptions block that was inside the guard (already set unconditionally above)

Summary by CodeRabbit

  • Chores
    • Centralized JVM target to Java 17 in Android build.
    • Updated Android Credentials libraries to v1.5.0.
    • Added a TypeScript annotation to suppress a WebAuthn type-check warning in web builds.
  • Bug Fixes
    • iOS: Avoids showing the cross-platform passkey picker when specific credentials are provided.
    • Android: Respects provided credentials to control auto-selection behavior during credential requests.

The kotlinOptions block was gated behind `agpVersion < 8`, so on AGP 8+
the Kotlin JVM target defaulted to whatever JDK was running Gradle.
With Kotlin 2.1.20 and JDK 20, this caused a JVM target mismatch
(Java 17 vs Kotlin 20) that fails the build.

Remove the AGP version check so jvmTarget=17 is always set.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Removed AGP-version conditional and always set kotlinOptions.jvmTarget = "17"; bumped Android Credentials libs; suppressed a TypeScript error on extensions in the web passkeys module; iOS now omits cross-platform assertion when allowCredentials is present; Android GetCredentialRequest now uses the Builder and sets autoSelectAllowed.

Changes

Cohort / File(s) Summary
Android build script
android/build.gradle
Removed AGP-version conditional that previously set nested compileOptions/kotlinOptions; now always sets kotlinOptions.jvmTarget = "17" and updated AndroidX Credentials deps to 1.5.0.
Android passkeys logic
android/src/main/java/expo/modules/passkeys/ReactNativePasskeysModule.kt
Replaced direct GetCredentialRequest construction with Builder pattern; computes hasAllowCredentials and calls setAutoSelectAllowed(hasAllowCredentials) before build().
Web passkeys module
src/ReactNativePasskeysModule.web.ts
Added // \@ts-expect-error`immediately before theextensionsproperty to silence a TypeScript mismatch forlargeBlob.write`; no runtime changes.
iOS passkeys auth flow
ios/ReactNativePasskeysModule.swift
Changed authorization request composition: when request.allowCredentials exists and is non-empty, construct controller with only the platform assertion request; otherwise include both platform and cross-platform requests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant JS as "JS Module"
  participant iOS as "iOS Module"
  participant ASAuth as "ASAuthorizationController"

  JS->>iOS: request assertion (includes request.allowCredentials?)
  alt allowCredentials present & non-empty
    iOS->>ASAuth: create controller with platformKeyAssertionRequest
  else
    iOS->>ASAuth: create controller with platformKeyAssertionRequest + crossPlatformKeyAssertionRequest
  end
  ASAuth->>iOS: delegate results
  iOS->>JS: return passkey result / error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • renanmav

Poem

🐇 I hopped through build and web and swift,
Set JVM to seventeen with a drift,
A tiny comment hushes the type,
iOS asks the key without a swipe,
Commit, munch carrot, off I skip.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: removing the AGP version check to always set Kotlin jvmTarget to 17 in android/build.gradle, which is the primary fix in this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Regenerate pnpm-lock.yaml to match current peerDependencies.
Fix TypeScript error where local AuthenticationExtensionsLargeBlobInputs
type's `write: string` conflicts with DOM's `write: BufferSource`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/ReactNativePasskeysModule.web.ts (1)

51-55: The @ts-expect-error scope is broader than ideal, but restructuring would require unwanted changes.

The suppression at line 51 covers the entire extensions: property, including the prf field. This is wider than the get flow's scoping (line 127), which narrows to the largeBlob property by constructing it explicitly with type-checked conversions.

However, narrowing the create flow's suppression to match get's pattern would require explicitly handling largeBlob.write (converting Base64URL to BufferSource). Per the WebAuthn spec, largeBlob.write is only valid in the get (assertion) flow, not create (attestation), making such conversion unnecessary and semantically incorrect.

The current broad suppression is a pragmatic trade-off: because largeBlob arrives opaquely via spread rather than through explicit construction, keeping the suppression at the property level is simpler than restructuring the code. The suppression will self-clean when the upstream type mismatch is resolved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ReactNativePasskeysModule.web.ts` around lines 51 - 55, The
`@ts-expect-error` currently precedes the entire extensions object (around the
extensions: { ...request.extensions, prf: normalizePRFInputs(request) }
construction) which is broader than necessary; either move the suppression to
only the prf line (apply `@ts-expect-error` directly to the
normalizePRFInputs(request) expression or its assignment) so you don't suppress
other extension fields, or if narrowing is impossible without restructuring,
replace the blanket suppression with a short explanatory comment referencing the
deliberate trade-off (largeBlob.write only applies to get/assertion and not
create/attestation) and keep the suppression scoped to the extensions property;
locate this change in ReactNativePasskeysModule.web.ts around the extensions
spread and the normalizePRFInputs(request) call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/ReactNativePasskeysModule.web.ts`:
- Around line 51-55: The `@ts-expect-error` currently precedes the entire
extensions object (around the extensions: { ...request.extensions, prf:
normalizePRFInputs(request) } construction) which is broader than necessary;
either move the suppression to only the prf line (apply `@ts-expect-error`
directly to the normalizePRFInputs(request) expression or its assignment) so you
don't suppress other extension fields, or if narrowing is impossible without
restructuring, replace the blanket suppression with a short explanatory comment
referencing the deliberate trade-off (largeBlob.write only applies to
get/assertion and not create/attestation) and keep the suppression scoped to the
extensions property; locate this change in ReactNativePasskeysModule.web.ts
around the extensions spread and the normalizePRFInputs(request) call.

When allowCredentials is specified, only include the platform assertion
request in ASAuthorizationController. Including the cross-platform
(SecurityKey) provider alongside the platform provider causes iOS to
show the expanded "choose how to sign in" UI instead of auto-selecting
the credential, breaking targeted assertions with third-party providers
like Chrome and 1Password.

Discoverable assertions (empty allowCredentials) still include both
providers for full compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/ReactNativePasskeysModule.swift`:
- Around line 52-61: The current branch that checks request.allowCredentials
only creates platformKeyAssertionRequest, preventing external/security-key
authenticators from matching; always include both platformKeyAssertionRequest
and the crossPlatformKeyAssertionRequest (from
prepareCrossPlatformAssertionRequest) in the ASAuthorizationController requests
when allowCredentials is non-empty, and set the allowedCredentials (credential
descriptors) on both request objects using request.allowCredentials so either
authenticator type can satisfy the assertion; only reduce to platform-only or
cross-platform-only when you have explicit verified metadata indicating the
credentials are exclusively one authenticator type.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8164cc2 and ae4186e.

📒 Files selected for processing (1)
  • ios/ReactNativePasskeysModule.swift

Comment on lines +52 to +61
if let allowCredentials = request.allowCredentials, !allowCredentials.isEmpty {
authController = ASAuthorizationController(authorizationRequests: [
platformKeyAssertionRequest,
])
} else {
let crossPlatformKeyAssertionRequest = prepareCrossPlatformAssertionRequest(
challenge: challengeData, request: request)
authController = ASAuthorizationController(authorizationRequests: [
platformKeyAssertionRequest, crossPlatformKeyAssertionRequest,
])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Apple ASAuthorizationPlatformPublicKeyCredentialAssertionRequest security key external credentials allowCredentials

💡 Result:

In Apple’s passkeys APIs, ASAuthorizationPlatformPublicKeyCredentialAssertionRequest.allowedCredentials is the native equivalent of WebAuthn allowCredentials: it lets you restrict an assertion to a specific set of credential IDs (non‑discoverable / “username-first” style sign-in). If you omit it or set it to an empty array, you enable the discoverable (passkey) experience where the system can show an account selector / pick from available passkeys without you pre-supplying credential IDs. [1], [2], [3]

Important distinction: platform vs external (security key) credentials

  • ASAuthorizationPlatformPublicKeyCredentialAssertionRequest is for the platform authenticator (passkeys in iCloud Keychain on Apple devices). [1]
  • For external / roaming authenticators (USB/NFC/BLE security keys), use ASAuthorizationSecurityKeyPublicKeyCredentialProvider and its ASAuthorizationSecurityKeyPublicKeyCredentialAssertionRequest, which also supports an allow-list via allowedCredentials (and some bindings expose additional related properties like SecurityAllowedCredentials). [4]

Typical patterns

1) Discoverable passkey sign-in (recommended UX): don’t set allowedCredentials

let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
let req = provider.createCredentialAssertionRequest(challenge: challenge)
// req.allowedCredentials = nil  // or []

Omitting/empty allowCredentials is what enables discoverable credential flows in WebAuthn/passkeys. [2], [3]

2) Restrict to known credential IDs (allow-list)

req.allowedCredentials = credentialIDs.map {
  ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: $0)
}

This mirrors WebAuthn allowCredentials usage. [2], [3]

3) External security key assertion: use the security-key provider
Conceptually:

  • create an ASAuthorizationSecurityKeyPublicKeyCredentialAssertionRequest
  • set its allowedCredentials to the security-key credential descriptors you want to permit (optionally including transport hints), then run it via ASAuthorizationController. [4]

Sources: Apple “Supporting passkeys” overview for the platform flow [1], WebAuthn/passkeys behavior of allowCredentials (omit/empty for discoverable credentials) [2], [3], and the security-key assertion request API surface showing AllowedCredentials for security keys [4].


🌐 Web query:

Apple AuthenticationServices ASAuthorizationSecurityKeyPublicKeyCredentialAssertionRequest vs platform request

💡 Result:

In AuthenticationServices, the assertion request type mainly selects which kind of WebAuthn authenticator you’re targeting:

  • ASAuthorizationPlatformPublicKeyCredentialAssertionRequest (“platform request”)

    • Targets a platform authenticator: a passkey stored on/managed by the Apple platform (e.g., iCloud Keychain / device-bound passkeys).
    • Can also surface hybrid / “use a phone” flows (QR/Bluetooth) on supported OS versions via platform-only UI controls like shouldShowHybridTransport (seen used only on the platform assertion request). [2]
    • In practice, this is the request you use for “Sign in with passkey” on Apple devices. [1]
  • ASAuthorizationSecurityKeyPublicKeyCredentialAssertionRequest (“security key request”)

    • Targets a cross‑platform external authenticator (a “security key”): USB/NFC/BLE FIDO2 keys (e.g., YubiKey).
    • Uses security‑key descriptors (ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor) rather than platform descriptors, and does not participate in the platform’s hybrid/passkey-on-phone UX toggles. [3][2]

What’s the practical difference in code?

  • You create them from different providers:
    • ASAuthorizationPlatformPublicKeyCredentialProvider(...).createCredentialAssertionRequest(challenge:) for platform/passkeys. [1][2]
    • ASAuthorizationSecurityKeyPublicKeyCredentialProvider(...).createCredentialAssertionRequest(challenge:) for external security keys. [2]

Common pattern (support both)

Many implementations create both a platform assertion request and a security-key assertion request (same RP ID + challenge), set allowedCredentials appropriately for each descriptor type, then pass both to ASAuthorizationController so the user can choose the authenticator. [2][3]

Sources: [1] [2] [3] [4]


🌐 Web query:

WebAuthn allowCredentials attribute effect on authenticator selection

💡 Result:

allowCredentials (in PublicKeyCredentialRequestOptions passed to navigator.credentials.get()) directly filters which authenticators are eligible by telling the user agent “only these credential IDs are acceptable for this assertion.”

  • If allowCredentials is non-empty: the browser will only let the user complete authentication using an authenticator that can produce an assertion for one of the listed credential IDs (and matching RP ID, etc.). If no available authenticator can satisfy any entry, the ceremony fails (commonly surfaced as NotAllowedError). [1], [2]
  • If allowCredentials is empty (or omitted): it means any credential is acceptable, which enables “username-less” / discoverable credential flows (the authenticator can choose an RP-matching discoverable credential and the browser may show an account picker). [1], [3]
  • transports inside each allowCredentials entry: are hints the platform can use to decide which authenticators could work (e.g., “internal” vs “usb/nfc/ble”), potentially avoiding presenting options that can’t satisfy the request. [2]

Practical effect on authenticator selection:

  • Populate allowCredentials when you already know the user (e.g., after username entry) to constrain authentication to that account’s registered credentials. [3]
  • Leave it empty/omitted only if you intend to support discoverable credentials and account selection on the authenticator/UI. [3]

Sources:
[1] MDN: PublicKeyCredentialRequestOptions.allowCredentials (restriction + empty array meaning “any credential”) (developer.mozilla.org)
[2] W3C WebAuthn Level 2: ineligible authenticators + transports hint and “no eligible credential” failure behavior (w3.org)
[3] W3C WebAuthn Level 3: definition/purpose of allowCredentials (“find authenticators eligible…”, recommended when account identified) (w3.org)

Citations:


Functional regression: non-empty allowCredentials prevents security-key authentication.

When allowCredentials is non-empty, the code now creates only a platform assertion request. If the allowCredentials list contains a credential ID registered to an external/security-key authenticator, that authenticator cannot fulfill the assertion—the platform request cannot produce a valid assertion for security-key credentials. This causes NotAllowedError.

Fix: Always provide both platform and security-key assertion requests when allowCredentials is set. Set allowedCredentials on each request type with appropriate descriptors, allowing either authenticator type to satisfy the credentials in the allow-list. Only use platform-only or security-key-only paths if there is explicit, verifiable information about which authenticator types the credentials belong to.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/ReactNativePasskeysModule.swift` around lines 52 - 61, The current branch
that checks request.allowCredentials only creates platformKeyAssertionRequest,
preventing external/security-key authenticators from matching; always include
both platformKeyAssertionRequest and the crossPlatformKeyAssertionRequest (from
prepareCrossPlatformAssertionRequest) in the ASAuthorizationController requests
when allowCredentials is non-empty, and set the allowedCredentials (credential
descriptors) on both request objects using request.allowCredentials so either
authenticator type can satisfy the assertion; only reduce to platform-only or
cross-platform-only when you have explicit verified metadata indicating the
credentials are exclusively one authenticator type.

Use GetCredentialRequest.Builder with setAutoSelectAllowed(true) when
allowCredentials is non-empty. This lets Android Credential Manager
skip the picker and go straight to biometric verification when a
single credential matches, preventing the hidden picker hang.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
android/src/main/java/expo/modules/passkeys/ReactNativePasskeysModule.kt (1)

78-78: Consider using isNullOrEmpty() for cleaner null-safe check.

The current pattern works correctly due to short-circuit evaluation, but Kotlin's standard library provides a more idiomatic approach.

♻️ Suggested improvement
-            val hasAllowCredentials = request.allowCredentials != null && request.allowCredentials!!.isNotEmpty()
+            val hasAllowCredentials = !request.allowCredentials.isNullOrEmpty()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/src/main/java/expo/modules/passkeys/ReactNativePasskeysModule.kt` at
line 78, Replace the null-and-empty check for allowCredentials with Kotlin's
idiomatic null-safe method: update the computation of hasAllowCredentials (which
currently uses request.allowCredentials != null &&
request.allowCredentials!!.isNotEmpty()) to call
request.allowCredentials.isNullOrEmpty() and invert as needed so
hasAllowCredentials remains true when there are credentials; reference the
hasAllowCredentials variable and request.allowCredentials to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/src/main/java/expo/modules/passkeys/ReactNativePasskeysModule.kt`:
- Around line 79-82: Remove the invalid setAutoSelectAllowed(...) call on
GetCredentialRequest.Builder in the block that builds getCredentialRequest;
instead only add GetPublicKeyCredentialOption(json) to the request builder, and
if you intended to enable client-side auto-select for a Google credential use
GetGoogleIdOption.Builder().setAutoSelectEnabled(true) and add that option to
the GetCredentialRequest via addCredentialOption(...) (or otherwise configure
auto-select on the appropriate provider entry builder), referencing
GetCredentialRequest.Builder, GetPublicKeyCredentialOption, and
GetGoogleIdOption.Builder.setAutoSelectEnabled.

---

Nitpick comments:
In `@android/src/main/java/expo/modules/passkeys/ReactNativePasskeysModule.kt`:
- Line 78: Replace the null-and-empty check for allowCredentials with Kotlin's
idiomatic null-safe method: update the computation of hasAllowCredentials (which
currently uses request.allowCredentials != null &&
request.allowCredentials!!.isNotEmpty()) to call
request.allowCredentials.isNullOrEmpty() and invert as needed so
hasAllowCredentials remains true when there are credentials; reference the
hasAllowCredentials variable and request.allowCredentials to locate the change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae4186e and 33e3289.

📒 Files selected for processing (1)
  • android/src/main/java/expo/modules/passkeys/ReactNativePasskeysModule.kt

The previous version (1.3.0-alpha01) did not have
setAutoSelectAllowed on GetCredentialRequest.Builder.
Version 1.5.0 (stable) includes this API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/build.gradle`:
- Around line 89-90: The project currently uses compileSdkVersion 33 while the
added dependencies implementation "androidx.credentials:credentials:1.5.0" and
implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
require minCompileSdk=35; fix by either updating compileSdkVersion to 35 in the
Android module's build.gradle (update the compileSdkVersion property) or replace
those dependency lines with versions compatible with compileSdkVersion 33
(downgrade the two androidx.credentials artifacts to a supported version);
ensure you update only the compileSdkVersion or the two dependency coordinates
(implementation "androidx.credentials:credentials:..." and implementation
"androidx.credentials:credentials-play-services-auth:...") so consumer builds no
longer fail.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33e3289 and 9250a80.

📒 Files selected for processing (1)
  • android/build.gradle

@tommeier
Copy link
Copy Markdown

looking for this too 👍

@friederbluemle
Copy link
Copy Markdown

Yes, completely agree that the jvm target needs to be fixed, but why does this PR contain 7 commits and 2,540 insertions/3,009 deletions? It should be a simple build.gradle fix.

@friederbluemle
Copy link
Copy Markdown

#60 contains the targeted fix (using the simpler kotlin.jvmToolchain method), ready for immediate merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants