Chris/mobile 343 bridge android to a native module that is available in the#7816
Chris/mobile 343 bridge android to a native module that is available in the#7816chriscanin wants to merge 22 commits intomainfrom
Conversation
- Implemented UserButton component to open UserProfileView on press. - Created UserProfile component for comprehensive profile management. - Integrated native ClerkExpo module for iOS functionality. - Updated ClerkProvider to configure Clerk iOS SDK. - Added exports for new components in the native index file. - Adjusted TypeScript configuration to include additional files. - Modified build process to temporarily skip declaration generation. - Updated dependencies in pnpm-lock.yaml for compatibility.
…d ClerkViewFactory
…ge-ios-to-a-native-module-that-is-available-in-the-expo
…profile management
…ge-android-to-a-native-module-that-is-available-in-the
…ling and UI presentation - Consolidated Clerk SDK initialization and session management in ClerkExpoModule. - Removed ClerkProfileActivity and replaced it with ClerkUserProfileActivity for better clarity and functionality. - Introduced ClerkViewFactory to manage creation of intents for authentication and user profile activities. - Enhanced error handling and promise management for asynchronous operations. - Updated SignIn and UserProfile components to synchronize native and JS session states effectively. - Improved user experience by ensuring the auth modal is always presented, allowing native UI to manage signed-in state. - Added backward-compatible wrappers for SignedIn and SignedOut components.
…and improved session handling
- Updated `clerk-android` versions in `build.gradle` to `0.1.30` for API and `0.1.4` for UI. - Added Kotlin metadata version check skip to address compatibility issues. - Introduced packaging exclusions for duplicate META-INF files in Android. - Enhanced `ClerkAuthActivity` to improve session handling and logging. - Updated `ClerkExpoModule` to include detailed logging for session retrieval. - Improved `ClerkUserProfileActivity` to handle sign-out detection and logging. - Refined `SignIn` and `UserProfile` components to prevent duplicate auth callbacks and improve user state management. - Added packaging exclusions in the Expo config plugin for Android to resolve dependency conflicts.
- Introduced AuthView component to handle sign-in and sign-up using native UI. - Added AuthView types for better type safety. - Removed deprecated SignIn component and its types. - Updated UserButton and UserProfileView components with enhanced documentation. - Refactored ClerkProvider to sync native sessions with JS SDK. - Adjusted TypeScript configurations for improved declaration generation.
…lement crash in React Native
… state management
🦋 Changeset detectedLatest commit: b8127fd The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
!snapshot |
|
Hey @chriscanin - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/agent-toolkit@0.2.9-snapshot.v20260210225422 --save-exact
npm i @clerk/astro@3.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/backend@3.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/chrome-extension@3.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/clerk-js@6.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/dev-cli@1.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/expo@3.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/expo-passkeys@1.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/express@2.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/fastify@2.6.9-snapshot.v20260210225422 --save-exact
npm i @clerk/localizations@4.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/msw@0.0.1-snapshot.v20260210225422 --save-exact
npm i @clerk/nextjs@7.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/nuxt@2.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/react@6.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/react-router@3.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/shared@4.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/tanstack-react-start@1.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/testing@2.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/ui@1.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/upgrade@2.0.0-snapshot.v20260210225422 --save-exact
npm i @clerk/vue@2.0.0-snapshot.v20260210225422 --save-exact |
📝 WalkthroughWalkthroughAdds comprehensive Expo native integration for Clerk on iOS and Android. Introduces native modules, view factories, activities, Expo config plugin and module config, Podspecs, Gradle updates, and Android manifest entries. Exposes native React Native components and inline native views (AuthView, InlineAuthView, UserButton, UserProfileView, InlineUserProfileView) with TypeScript typings and hooks (useNativeSession, useNativeAuthEvents). Implements a headless initialization path in the isomorphic loader to avoid loading browser UI in native/headless contexts. Includes docs and changelogs for the native components and quality fixes. 🚥 Pre-merge checks | ✅ 4 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. No actionable comments were generated in the recent review. 🎉 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
🤖 Fix all issues with AI agents
In `@packages/expo/android/build.gradle`:
- Around line 50-56: Update the clerk Android UI dependency from 0.1.4 to 0.1.5
in the Gradle dependency declaration (replace the artifact version referencing
clerk-android-ui) and remove the temporary Kotlin compiler bypass by deleting
the "-Xskip-metadata-version-check" entry from the
kotlinOptions.freeCompilerArgs block (the block that contains jvmTarget = "17"
and freeCompilerArgs). Ensure the project no longer relies on the workaround so
the kotlinOptions section only sets the desired jvmTarget and the dependency now
points to clerk-android-ui:0.1.5.
In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthActivity.kt`:
- Around line 368-375: Remove the deprecated onBackPressed override from
ClerkAuthActivity (the override function named onBackPressed currently checking
EXTRA_DISMISSABLE and calling setResult/ super.onBackPressed); rely on the
existing Compose BackHandler (lines ~297-305 in this file) to handle back
presses instead, and scan ClerkAuthActivity for any callers that relied on the
onBackPressed override to ensure they use the Compose handler or explicit
finish/setResult logic instead.
- Around line 219-234: The polling loop and the LaunchedEffect(session) path can
race and both call setResult/finish because they check and set the Boolean
isAuthComplete non-atomically; replace that guard with an atomic check-or-set
(e.g., use an AtomicBoolean similar to the dismissed flag in
ClerkUserProfileActivity) or consolidate session-complete logic into a single
handler so both paths perform a single atomic compareAndSet on a shared
AtomicBoolean (referencing isAuthComplete, the LaunchedEffect(session) block,
the polling loop that reads Clerk.session, and the setResult/finish calls) to
ensure only one path proceeds to call setResult and finish().
- Around line 96-102: ClerkAuthActivity is logging PII (emails, phone numbers,
names, MFA details and raw IDs) via Log.d of TAG in places that include
client.signIn, client.signUp, signUp.emailAddress, supportedSecondFactors
entries, sessions and periodic polling logic; remove or redact PII and gate
verbose debug logs behind a debug flag (e.g., BuildConfig.DEBUG) or a runtime
debug toggle before shipping, replace direct PII fields with redacted
placeholders or hashed IDs, stop logging full supportedSecondFactors objects
(log only non-PII enums/counts), and disable the 2-second periodic full-client
dump (or make it conditional on debug mode) so only safe, non-identifying
diagnostics are emitted from ClerkAuthActivity.
In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`:
- Around line 160-213: The getSession AsyncFunction is currently logging PII
(user.firstName, user.lastName, user.imageUrl, user.emailAddresses) via
Log.d(TAG, ...) which is a privacy/compliance risk; remove those Log.d calls or
guard them behind a strict debug check (e.g., BuildConfig.DEBUG or a
module-level debug flag) inside AsyncFunction("getSession") so PII is never
logged in production, and if you need observability log only non-PII indicators
(e.g., "user present" or hashed/anonymized identifiers) using the existing TAG
and Clerk.session/Clerk.user symbols.
- Around line 67-69: The static promise fields pendingAuthPromise and
pendingProfilePromise can be silently overwritten by a second call to
presentAuth/presentProfile, and configure() awaits Clerk.isInitialized.first {
it } indefinitely if initialization never becomes true; before assigning a new
Promise in presentAuth/presentProfile reject the existing
pendingAuthPromise/pendingProfilePromise with an appropriate error and clear it
to avoid leaking unresolved JS promises, and in configure() replace the
unbounded first { it } wait with a bounded wait (use
kotlinx.coroutines.withTimeout) or concurrently observe
Clerk.initializationError and reject the configure promise if an error is
emitted so configure() always resolves or rejects; reference the symbols
pendingAuthPromise, pendingProfilePromise, presentAuth, presentProfile,
configure, Clerk.isInitialized, Clerk.initializationError and add the
withTimeout import.
In `@packages/expo/app.plugin.js`:
- Around line 277-316: The lookup paths in the possiblePaths array are using the
wrong package directory name; update the two paths that reference
'@clerk/clerk-expo' to use the correct '@clerk/expo' so the search for
ClerkViewFactory.swift (constructed from config.modRequest.projectRoot) will
succeed for normal npm/yarn and pnpm-hoisted installs; keep the rest of the
possiblePaths entries and the existing fs.existsSync check and sourceFile
assignment unchanged.
In `@packages/expo/ios/ClerkExpoModule.swift`:
- Around line 44-62: The continuation can never be resumed if
UIApplication.shared.keyWindow?.rootViewController is nil; update the code paths
in the auth presentation routine (the block that calls
factory.createAuthViewController(...) and the similar presentUserProfile
function) to handle a missing root view controller by immediately resuming the
checked continuation with a descriptive error (e.g., NSError with domain
"ClerkExpo" and an appropriate code/message) before returning, and ensure both
the success/failure from the completion closure and the "cannot present" error
are the only places that resume the continuation to avoid leaks or
double-resumes.
In `@packages/expo/ios/ClerkViewFactory.swift`:
- Around line 158-180: The subscribeToAuthEvents Task (authEventTask) can exit
the for-await loop or encounter events with nil createdSessionId without calling
the completion closure, leaking the continuation in ClerkExpoModule.swift;
update subscribeToAuthEvents in ClerkViewFactory.swift to ensure completion is
always invoked by (1) calling completion(.failure(...)) with a descriptive error
when the async sequence terminates naturally (after the for await loop finishes)
or when the Task is cancelled/deinit, and (2) treating nil createdSessionId
paths by invoking completion(.failure(...)) instead of silently skipping; apply
the same pattern to ClerkProfileWrapperViewController (the signedOut handler)
and ensure any place that relies on withCheckedThrowingContinuation is covered
so the continuation is always resumed exactly once.
In `@packages/expo/src/native/AuthView.tsx`:
- Line 281: The effect that mounts and calls presentModal in AuthView is
depending on onSuccess and onError which can be re-created each render; update
by removing onSuccess and onError from the useEffect dependency array and
instead store their latest values in refs (e.g., successRef.current and
errorRef.current), update those refs whenever props change, and have
presentModal read and invoke successRef.current / errorRef.current so the effect
won't re-run on every render but will still call the latest callbacks; keep
hasStartedRef logic as-is to prevent re-presentation.
- Around line 249-273: The current fragile string-match in AuthView (if
(error.message?.includes('already signed in'))) should be replaced with a robust
detection helper (e.g., isAlreadySignedInError) that first checks for structured
properties (error.code, error.name, error.type) returned by the native bridge
and falls back to multiple tolerant message patterns only if structured fields
are absent; update the recovery block that calls ClerkExpo.getSession and
clerk.setActive to use this helper, wrap the entire fallback in a try/catch with
explicit processLogger/console.error messages including the original error and
recovery attempt context, and reference symbols: AuthView,
isAlreadySignedInError (new), ClerkExpo.getSession, clerk.setActive,
authCompletedRef, and onSuccess.
In `@packages/expo/src/native/README.md`:
- Around line 144-153: Update the README usage example to import from the actual
package and use the correct component names: change the import path from
"@clerk/clerk-expo/native" to "@clerk/expo" and replace the SignIn and
UserProfile usages with the actual exported names AuthView and UserProfileView
(keep UserButton if still exported); update the JSX example to render <AuthView
/> (and <UserProfileView /> where shown) so symbols match the package exports.
In `@packages/expo/src/native/UserProfileView.tsx`:
- Around line 216-228: The code accesses an undocumented internal API
(__internal_reloadInitialResources) on the clerk object after signOut, which is
fragile; remove the cast and the await
clerkAny.__internal_reloadInitialResources() block from UserProfileView (and the
surrounding console logs) and instead: rely solely on the public clerk.signOut()
call for sign-out recovery, keep the existing try/catch around signOut to log
errors, and add a TODO comment referencing coordinating with the `@clerk/react`
SDK team to expose a supported recovery/reload method or to implement an
officially supported workaround if needed.
- Around line 170-244: The effect re-opens the native modal because isSignedIn
and possibly onSignOut are in the dependency array and signOutTriggered.current
is reset at the start of every run; fix by removing isSignedIn and onSignOut
from the useEffect deps and instead capture their latest values via refs (create
isSignedInRef.current = isSignedIn and onSignOutRef.current = onSignOut and use
those inside presentModal), only run the effect when isDismissable changes (use
[isDismissable] as the dependency array), and stop resetting
signOutTriggered.current at the top of this effect (reset it once on mount using
a separate useEffect(()=>{ signOutTriggered.current = false }, []) so the
sign-out guard persists across the sign-out transition).
In `@packages/expo/src/provider/ClerkProvider.tsx`:
- Around line 109-121: The code relies on undocumented internals by casting
clerkInstance to any and using clerkAny.loaded, clerkAny.addOnLoaded and
clerkAny.__internal_reloadInitialResources; update waitForLoad and the place
where setActive is called (reference: clerkInstance, waitForLoad, setActive,
clerkAny.loaded, clerkAny.addOnLoaded, __internal_reloadInitialResources) to
perform robust runtime guards: after waitForLoad resolves, explicitly verify the
client is ready using safe runtime checks (e.g. check for a documented public
readiness flag or, if absent, confirm clerkAny.loaded === true before calling
setActive), and if the ready check fails, avoid calling setActive and handle
fallback (retry, no-op, or surface an error) so the code won't call internals or
setActive prematurely when these internal properties are missing or changed.
- Around line 77-162: The async configureNativeClerk() starts fire-and-forget
polling and may call clerkInstance.setActive after unmount; add an
AbortController or isMountedRef to cancel work: create e.g. const abortCtrl =
new AbortController() / const mountedRef = useRef(true) and update it in the
effect cleanup, check abortCtrl.signal.aborted or !mountedRef.current inside the
polling loop and before awaiting waitForLoad and before calling setActive to
bail out early, clear any pending setTimeout waits, and if you attach a listener
via clerkAny.addOnLoaded, store and remove that listener in the cleanup so it
cannot fire after unmount; return a cleanup from the useEffect that aborts the
controller (or sets mountedRef false) and resets initStartedRef.current if
appropriate. Ensure references named in the diff (configureNativeClerk,
clerkInstance, clerkAny, pendingNativeSessionRef, sessionSyncedRef,
initStartedRef) are checked for the abort flag before mutating state or calling
setActive.
In `@packages/react/src/isomorphicClerk.ts`:
- Around line 308-316: The premount listener invocation is using an unsafe "as
any" cast on currentState; replace that with a proper Partial<State> (or
construct a full State with safe defaults) so ListenerCallback consumers get
correct typing. Specifically, change the value passed to
this.premountAddListenerCalls.forEach(listener => listener(...)) to be typed as
Partial<State> instead of any (or build a complete State by merging
clerk.client/session/user/organization with default no-op signIn/signUp/etc.),
and update usages around premountAddListenerCalls, currentState, and
ListenerCallback accordingly to remove the any cast while preserving runtime
behavior.
- Around line 323-331: The current catch on clerk.load silently swallows errors
and still calls finishInit(), causing `#initialized` to become true and emitting
clerkEvents.Status 'ready' even on failure; update the catch handler in the
isomorphicClerk initialization to log the error (include the caught error
object), set a failure/degraded status via clerkEvents.Status (e.g., 'error' or
'degraded') instead of 'ready', and ensure any state set by finishInit (such as
marking `#initialized` or assigning the clerk instance) reflects the failed load
so consumers can distinguish success from failure; modify the promise chain
around clerk.load(this.options) and the finishInit() logic to branch on success
vs failure using the unique symbols clerk.load, finishInit, and
clerkEvents.Status.
| kotlinOptions { | ||
| jvmTarget = "17" | ||
| // Skip Kotlin metadata version check to work around clerk-android being compiled with Kotlin 2.3.0 | ||
| // while Expo uses Kotlin 2.1.20. This is a temporary workaround until clerk-android publishes | ||
| // artifacts compatible with Kotlin 2.1.x | ||
| freeCompilerArgs += ["-Xskip-metadata-version-check"] | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
clerk-android SDK Kotlin 2.1 2.3 version compatibility
💡 Result:
Clerk’s Android SDK is sensitive to your project’s Kotlin compiler version (because the SDK is published as Kotlin-compiled artifacts).
- If your app is on Kotlin 2.1.x: use
com.clerk:clerk-android-api:0.1.31(andclerk-android-ui:0.1.5), released Jan 29, 2026, which explicitly downgrades Kotlin/deps to work on Kotlin 2.1.20. [1] - If your app is on Kotlin 2.3.x: use a newer Clerk Android SDK release after 0.1.31, because the repo updates the Kotlin toolchain to Kotlin 2.3.0 and 2.3.10 in later releases (so those artifacts are intended to be built/consumed with Kotlin 2.3). [2]
Also note the baseline project requirements in Clerk’s Android quickstart: minSdk 24+ and Java 17+. [3]
Sources
[1] Clerk Android SDK release v0.1.31 (notes include Kotlin 2.1.20 compatibility + module versions) https://github.com/clerk/clerk-android/releases
[2] Clerk Android SDK latest release notes showing Kotlin updated to 2.3.0 / 2.3.10 https://github.com/clerk/clerk-android/releases
[3] Clerk Android Quickstart (minSdk 24+, Java 17+) https://clerk.com/docs/android/getting-started/quickstart
🌐 Web query:
Kotlin 2.1.20 vs 2.3.0 binary compatibility metadata version check
💡 Result:
Kotlin/JVM libraries carry Kotlin metadata (in the @Metadata annotation and related structures). Tooling (the Kotlin compiler itself, KSP/kapt processors, analyzers like Dagger/Hilt/Room, etc.) must be able to read that metadata to typecheck and do symbol processing.
Why Kotlin 2.1.20 vs 2.3.0 can fail
- For JVM classfiles, the metadata version tracks the Kotlin language version (since Kotlin 1.4). So code compiled with Kotlin 2.3.0 typically has metadata version 2.3.0, while Kotlin 2.1.20 produces/“expects” 2.1.x-era metadata. [1]
- An older compiler/tool generally reads older metadata fine, but may only read a limited range “forward” (often ~one minor), and will error once the metadata is too new (the classic “compiled with an incompatible version of Kotlin / metadata is X, expected Y” failure mode). [2]
- Similarly, libraries that parse metadata via
kotlin-metadata-jvmcan fail fast:readStrict()is intentionally limited and supports only up to about one version beyond what yourkotlin-metadata-jvmversion considers “latest stable supported”; otherwise it throws to avoid mis-parsing unknown formats. [3]
What to do (practical fixes)
-
Best fix: align versions
Compile/consume with Kotlin 2.3.x if you depend on libraries compiled with 2.3.0 metadata (or downgrade those libraries to a Kotlin 2.1/2.2-compatible build). -
If the failure is from an annotation processor / analyzer (Dagger/Hilt/Room/etc.)
Update the tool so it uses a newer metadata parser. This is exactly the kind of issue reported when a tool supports up to metadata 2.2.0 but encounters 2.3.0. [4] -
If you directly use
kotlin-metadata-jvm/kotlinx-metadata-jvm
Upgrade to a version new enough to understand Kotlin 2.3 metadata (for exampleorg.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0). [5]
Rule of thumb: Kotlin 2.1.20 tooling reading Kotlin 2.3.0-produced metadata is not guaranteed to work; upgrade the compiler/tooling or downgrade the producer.
Sources
[1] https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-jvm-metadata-version/
[2] https://stackoverflow.com/questions/79176331/what-caused-module-was-compiled-with-an-incompatible-version-of-kotlin-error
[3] https://kotlinlang.org/docs/metadata-jvm.html
[4] google/dagger#5001
[5] https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-metadata-jvm/2.3.0
🌐 Web query:
-Xskip-metadata-version-check Kotlin compiler flag when necessary
💡 Result:
-Xskip-metadata-version-check tells the Kotlin compiler to ignore Kotlin @Metadata version incompatibility when it loads classes compiled by a different/newer Kotlin compiler (i.e., “compiled with an incompatible version of Kotlin / metadata version … expected …”). This bypasses a safety check around Kotlin’s binary metadata format/versioning. [1][2]
Use it only when it’s truly necessary (temporary workaround)
Use -Xskip-metadata-version-check only as a short-term unblocker when you can’t immediately align versions, for example:
- You must consume a dependency built with a newer/pre-release Kotlin compiler than your project’s compiler (common during EAP / fast-moving multiplatform or library publishing). Kotlin team members explicitly suggested this as an “easiest solution” workaround in such cases. [1]
- You’re stuck with tooling that reads Kotlin metadata (annotation processors / code generators) and you hit “unsupported metadata version” errors due to version skew (e.g., older processors vs newer Kotlin). Dagger/Hilt issues show this exact failure mode (“Unsupported metadata version…”). [3][4]
Prefer fixing the root cause instead
Before using the flag, first try (in order):
- Upgrade your Kotlin Gradle plugin / compiler to match (or exceed) the Kotlin version used to compile the problematic dependency. (This is the intended fix for “incompatible metadata version”.)
- Upgrade the tool that reads metadata (e.g., Hilt/Dagger/other processors) so it supports your Kotlin version (these failures are widely reported as version-support gaps). [3][4]
- If you can’t upgrade, downgrade the dependency to one compiled with an older Kotlin version compatible with your project.
How to add it (Gradle)
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.add("-Xskip-metadata-version-check")
}Risk
Skipping the check can let the compiler (or metadata-reading tools) proceed with unknown/newer metadata formats, which is exactly what the check is designed to prevent. Kotlin’s metadata versioning exists because format changes can break readers. [2]
Rule of thumb: enable it only to unblock builds briefly, and remove it once you’ve aligned Kotlin + dependencies/tooling.
Sources
[1] Kotlin Discussions (JetBrains): workaround suggestion to pass -Xskip-metadata-version-check when a library is built with an earlier/different compiler pre-release. (discuss.kotlinlang.org)
[2] Kotlin kotlinx-metadata-jvm docs: metadata versioning and compatibility expectations. (kotlinlang.org)
[3] Google Dagger issue: “Hilt incompatible… Unsupported metadata version…” (example of metadata reader mismatch). (github.com)
[4] Google Dagger issue: “Unsupported metadata version…” tied to old kotlinx-metadata-jvm support vs newer Kotlin. (github.com)
Citations:
- 1: https://discuss.kotlinlang.org/t/kotlin-1-3-m2-new-multiplatform-projects-model/9264/29?utm_source=openai
- 2: https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata.jvm/-jvm-metadata-version/?utm_source=openai
- 3: Hilt incompatible with Kotlin to 1.9 google/dagger#4035
- 4: Unsupported metadata version when using Hilt 2.42 with Kotlin 1.7.10 google/dagger#3470
Update clerk-android-ui to version 0.1.5 to eliminate the metadata version workaround.
The Clerk Android SDK officially provides clerk-android-ui:0.1.5 (released Jan 29, 2026) built with Kotlin 2.1.20 for compatibility with your project's Kotlin version. Replace version 0.1.4 with 0.1.5 and remove the -Xskip-metadata-version-check flag. Using an outdated dependency version with a compiler safety bypass is fragile and masks incompatibilities that should be resolved at the dependency level.
🤖 Prompt for AI Agents
In `@packages/expo/android/build.gradle` around lines 50 - 56, Update the clerk
Android UI dependency from 0.1.4 to 0.1.5 in the Gradle dependency declaration
(replace the artifact version referencing clerk-android-ui) and remove the
temporary Kotlin compiler bypass by deleting the "-Xskip-metadata-version-check"
entry from the kotlinOptions.freeCompilerArgs block (the block that contains
jvmTarget = "17" and freeCompilerArgs). Ensure the project no longer relies on
the workaround so the kotlinOptions section only sets the desired jvmTarget and
the dependency now points to clerk-android-ui:0.1.5.
packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthActivity.kt
Outdated
Show resolved
Hide resolved
packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthActivity.kt
Show resolved
Hide resolved
packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthActivity.kt
Outdated
Show resolved
Hide resolved
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
Show resolved
Hide resolved
| const clerkAny = clerkInstance as any; | ||
|
|
||
| const waitForLoad = (): Promise<void> => { | ||
| return new Promise(resolve => { | ||
| if (clerkAny.loaded) { | ||
| resolve(); | ||
| } else if (typeof clerkAny.addOnLoaded === 'function') { | ||
| clerkAny.addOnLoaded(() => resolve()); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Fragile reliance on undocumented internal Clerk APIs via as any casts.
clerkAny.loaded, clerkAny.addOnLoaded, and clerkAny.__internal_reloadInitialResources (Lines 109-135) are accessed through as any casts. If any of these internal APIs change in a Clerk SDK update, this code will silently fail or throw at runtime. The waitForLoad function at Lines 111-121 resolves immediately if neither loaded nor addOnLoaded exists, which could cause setActive to be called before the client is ready.
At minimum, add a runtime check before calling setActive that the client is actually loaded, to avoid a hard failure if these internals change.
🤖 Prompt for AI Agents
In `@packages/expo/src/provider/ClerkProvider.tsx` around lines 109 - 121, The
code relies on undocumented internals by casting clerkInstance to any and using
clerkAny.loaded, clerkAny.addOnLoaded and
clerkAny.__internal_reloadInitialResources; update waitForLoad and the place
where setActive is called (reference: clerkInstance, waitForLoad, setActive,
clerkAny.loaded, clerkAny.addOnLoaded, __internal_reloadInitialResources) to
perform robust runtime guards: after waitForLoad resolves, explicitly verify the
client is ready using safe runtime checks (e.g. check for a documented public
readiness flag or, if absent, confirm clerkAny.loaded === true before calling
setActive), and if the ready check fails, avoid calling setActive and handle
fallback (retry, no-op, or surface an error) so the code won't call internals or
setActive prematurely when these internal properties are missing or changed.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@packages/expo/ios/ClerkExpoModule.swift`:
- Around line 61-69: The inline views are being wrapped in a new plain
UIViewController even though factory.createAuthView(...) /
factory.createUserProfileView(...) should return the original
UIHostingController; update updateView() to accept a UIViewController? from the
factory and, when non-nil, attach that returned controller (not its view) as the
child: addChild(returnedController), set
returnedController.view.frame/autoresizingMask,
addSubview(returnedController.view), call didMove(toParent:), and store it in
hostingController; remove the creation of a new UIViewController so the SwiftUI
lifecycle from the original hosting controller is preserved (apply same change
for the other occurrence around hostingController usage).
In `@packages/expo/ios/templates/ClerkViewFactory.swift`:
- Around line 102-111: The returned SwiftUI views are leaking lifecycle because
createAuthView and createUserProfileView construct a local UIHostingController,
return only hostingController.view, and let the controller be deallocated;
change the factory to return the hosting controller itself (or change the
protocol signature to return UIViewController? instead of UIView) so the caller
can retain it, and update ClerkExpoModule (specifically
ClerkAuthExpoView.updateView() and ClerkUserProfileExpoView.updateView()) to add
the returned UIHostingController as a child view controller (retain via
addChild(_:), view containment and didMove(toParent:)) rather than wrapping only
the view.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`:
- Around line 230-241: The getClientToken AsyncFunction currently reads
undocumented SharedPreferences keys ("clerk_preferences"/"DEVICE_TOKEN");
replace that with the Clerk Android SDK public API by calling
Clerk.session?.fetchToken() from inside getClientToken, check for null session,
handle the fetchToken() result by resolving promise.resolve(token.jwt) on
success and promise.resolve(null) on failure or when session/result is null;
update error handling/logging accordingly and remove direct SharedPreferences
access.
In
`@packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserProfileExpoView.kt`:
- Around line 42-56: The ComposeView setup in ClerkUserProfileExpoView (and
similarly in ClerkAuthExpoView) starts a Recomposer and a
CoroutineScope(recomposerContext) that are never cancelled, leaking coroutines
and Recomposers; fix by making the Recomposer and a CoroutineScope (backed by a
Job or SupervisorJob) instance properties on the view (e.g., properties named
recomposer and recomposerScope), start runRecomposeAndApplyChanges() via
recomposerScope.launch, and override onDetachedFromWindow() to cancel/cleanup:
call recomposer.cancel(), cancel the recomposerScope/job, and then call
super.onDetachedFromWindow(); ensure the view teardown logic mirrors this for
ClerkAuthExpoView as well.
In `@packages/expo/src/native/AuthView.tsx`:
- Around line 129-151: syncNativeSession currently uses a non-null assertion on
getClerkInstance() and an any-cast to call an internal API
(__internal_reloadInitialResources), which risks runtime crashes or silent
failures; change it to null-check the clerk instance returned by
getClerkInstance() and bail out early (or handle the error) if missing, and
replace the any-cast with a safe type/guard: check typeof
clerkInstance.__internal_reloadInitialResources === 'function' (or use optional
chaining) before calling it so you don't cast to any, and add a small fallback
(e.g., attempt a public refresh API like setActive without the reload or emit a
debug log) so token caching (SecureStore.setItemAsync with CLERK_CLIENT_JWT_KEY
after ClerkExpo.getClientToken) won't silently leave the SDK stale if the
internal method is removed.
- Around line 226-244: The code currently sets authCompletedRef.current = true
and calls onSuccessRef.current?.() regardless of whether
syncNativeSession(result.sessionId) succeeds, which can leave the JS SDK
unauthenticated; change the flow so that authCompletedRef.current is set only
after a successful sync and if syncNativeSession throws call onErrorRef.current
with the sync error (or implement a retry loop inside the sync block) and return
early to avoid invoking onSuccessRef.current; specifically update the block
around syncNativeSession, authCompletedRef, and onSuccessRef (and use
onErrorRef.current for error propagation) so onSuccess is only invoked after
syncNativeSession completes successfully.
In `@packages/expo/src/provider/ClerkProvider.tsx`:
- Around line 194-223: The async callback syncNativeAuthToJs inside the second
useEffect must honor the same isMountedRef guard used elsewhere: before
performing any side-effecting calls and after every await, check
isMountedRef.current and return early if false to avoid mutating the singleton
after unmount; specifically add isMountedRef.current checks around/after
awaiting clerkAny.__internal_reloadInitialResources(), await
clerkInstance.setActive({ session: ... }), and await clerkInstance.signOut() so
those calls are skipped if the component has unmounted.
In `@packages/react/src/isomorphicClerk.ts`:
- Around line 303-305: The forEach callback on this.premountAddListenerCalls
currently returns the value of clerk.addListener(listener) via the assignment to
listenerHandlers.nativeUnsubscribe, which triggers the useIterableCallbackReturn
lint; change the callback to a block statement so it does not return a value
(e.g., use braces and perform the assignment as a standalone statement) and
ensure the assignment stays on listenerHandlers.nativeUnsubscribe using the same
symbols (this.premountAddListenerCalls, listenerHandlers, clerk.addListener).
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
Outdated
Show resolved
Hide resolved
| private val composeView = ComposeView(context).also { view -> | ||
| activity?.let { act -> | ||
| view.setViewTreeLifecycleOwner(act) | ||
| view.setViewTreeViewModelStoreOwner(act) | ||
| view.setViewTreeSavedStateRegistryOwner(act) | ||
|
|
||
| val recomposerContext = AndroidUiDispatcher.Main | ||
| val recomposer = Recomposer(recomposerContext) | ||
| view.setParentCompositionContext(recomposer) | ||
| CoroutineScope(recomposerContext).launch { | ||
| recomposer.runRecomposeAndApplyChanges() | ||
| } | ||
| } | ||
| addView(view, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) | ||
| } |
There was a problem hiding this comment.
Recomposer and CoroutineScope are never cancelled — resource leak on view detach.
The CoroutineScope(recomposerContext) at line 51 has no Job and is never cancelled when the view is removed from the hierarchy. The Recomposer will continue running runRecomposeAndApplyChanges() indefinitely. If this view is mounted/unmounted multiple times, each cycle leaks a coroutine and recomposer. The same pattern exists in ClerkAuthExpoView.kt (lines 52–57).
Consider overriding onDetachedFromWindow() to cancel the recomposer, or use a SupervisorJob that you cancel on detach.
🤖 Prompt for AI Agents
In
`@packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserProfileExpoView.kt`
around lines 42 - 56, The ComposeView setup in ClerkUserProfileExpoView (and
similarly in ClerkAuthExpoView) starts a Recomposer and a
CoroutineScope(recomposerContext) that are never cancelled, leaking coroutines
and Recomposers; fix by making the Recomposer and a CoroutineScope (backed by a
Job or SupervisorJob) instance properties on the view (e.g., properties named
recomposer and recomposerScope), start runRecomposeAndApplyChanges() via
recomposerScope.launch, and override onDetachedFromWindow() to cancel/cleanup:
call recomposer.cancel(), cancel the recomposerScope/job, and then call
super.onDetachedFromWindow(); ensure the view teardown logic mirrors this for
ClerkAuthExpoView as well.
|
!snapshot |
- packages/expo/package.json: merge new exports (./types) and file entries (google, apple) - packages/expo/src/hooks/index.ts: use main's standard re-exports for useSignIn/useSignUp/useWaitlist - packages/expo/src/provider/singleton/createClerkInstance.ts: use @clerk/clerk-js import (not headless subpath) - packages/react/src/isomorphicClerk.ts: use main's ClerkUI loading pattern via options.ui
|
Hey @chriscanin - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/agent-toolkit@0.2.9-snapshot.v20260213054637 --save-exact
npm i @clerk/astro@3.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/backend@3.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/chrome-extension@3.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/clerk-js@6.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/dev-cli@1.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/expo@3.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/expo-passkeys@1.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/express@2.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/fastify@2.6.9-snapshot.v20260213054637 --save-exact
npm i @clerk/localizations@4.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/msw@0.0.1-snapshot.v20260213054637 --save-exact
npm i @clerk/nextjs@7.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/nuxt@2.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/react@6.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/react-router@3.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/shared@4.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/tanstack-react-start@1.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/testing@2.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/ui@1.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/upgrade@2.0.0-snapshot.v20260213054637 --save-exact
npm i @clerk/vue@2.0.0-snapshot.v20260213054637 --save-exact |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@packages/expo/ios/ClerkExpoModule.swift`:
- Around line 176-197: The code can double-resume the continuation because
factory.createAuthViewController is called (and its completion captures the
continuation) before verifying there is a root view controller; if rootVC is nil
you resume the continuation and the VC’s deinit may later call the same
continuation. Fix by checking for
UIApplication.shared.keyWindow?.rootViewController (or equivalent safe root)
before calling factory.createAuthViewController in the auth flow and in
presentUserProfile, returning/throwing the NSError immediately if no root
exists; only create and present the view controller (via
factory.createAuthViewController / the user profile factory method) after
confirming a non-nil rootVC so the VC’s completion cannot race to resume the
same continuation twice.
In `@packages/expo/ios/ClerkViewFactory.swift`:
- Around line 196-229: The task can cancel when the VC is deallocated which
makes `self` nil and prevents calling `completion`, leaving the awaiting
continuation trapped; fix by capturing the VC's completion closure by value
before creating the Task (e.g. let capturedCompletion = self.completion), then
use that capturedCompletion inside subscribeToAuthEvents' Task instead of going
through self so the failure/resolution path after the for-await and on
cancellation will always call the continuation; apply the same change to the
profile event wrapper (the analogous subscribeToProfileEvents/profileEventTask
block).
- Around line 87-125: The two methods createAuthView and createUserProfileView
currently return UIView? which mismatches ClerkViewFactoryProtocol (expects
UIViewController?); change their return types to UIViewController? and return
the UIHostingController instance (hostingController) instead of
hostingController.view, keeping any view configuration (e.g.,
hostingController.view.backgroundColor = .clear) intact so callers in
ClerkExpoModule can attach the view controller as a child.
In `@packages/expo/ios/templates/ClerkViewFactory.swift`:
- Line 163: NSLog in ClerkViewFactory.swift is writing PII (user.firstName and
user.lastName) to device logs; remove the direct name logging or gate it so it
never logs names in production. Replace the NSLog call that references
user.firstName/user.lastName in the ClerkViewFactory codepath with either (a) no
log of names, (b) a log guarded by a debug-only build flag (`#if` DEBUG) or a
runtime canLogPII check, or (c) log non-PII information (e.g., anonymized user
id or status) instead. Ensure the change targets the NSLog call that references
user.firstName and user.lastName so production builds do not emit PII.
- Around line 29-35: The configure() function currently swallows errors from
Clerk.shared.load() by catching and only printing them, so change it to
propagate the failure instead of continuing: remove the do/catch that swallows
the error (or rethrow inside the catch) and call try await Clerk.shared.load()
directly (or rethrow the caught error) so the async throws signature of
configure() surfaces load() failures to callers; keep the logging around the
successful path but ensure any load() error is rethrown to the caller.
In `@packages/react/src/isomorphicClerk.ts`:
- Around line 332-338: The catch block for clerk.load currently calls
this.emitLoaded(), which resolves `#waitForClerkJS`() with a null this.clerkjs and
causes NPEs; update the catch in isomorphicClerk.ts (the clerk.load().catch
handler) to NOT call this.emitLoaded() on failure—only emit the error status
(this.#eventBus.emit(clerkEvents.Status, 'error')) and return so loaded
listeners are not resolved with null; alternatively, if you prefer preserving
resolution, change `#waitForClerkJS` to reject/throw when this.clerkjs is null,
but prefer removing the emitLoaded() call in the clerk.load error path
referenced by emitLoaded, `#waitForClerkJS`, clerk.load, this.clerkjs, and
`#initialized`.
| return try await withCheckedThrowingContinuation { continuation in | ||
| DispatchQueue.main.async { | ||
| guard let vc = factory.createAuthViewController(mode: mode, dismissable: dismissable, completion: { result in | ||
| switch result { | ||
| case .success(let data): | ||
| continuation.resume(returning: data) | ||
| case .failure(let error): | ||
| continuation.resume(throwing: error) | ||
| } | ||
| }) else { | ||
| continuation.resume(throwing: NSError(domain: "ClerkExpo", code: 2, userInfo: [NSLocalizedDescriptionKey: "Could not create auth view controller"])) | ||
| return | ||
| } | ||
|
|
||
| if let rootVC = UIApplication.shared.keyWindow?.rootViewController { | ||
| rootVC.present(vc, animated: true) | ||
| } else { | ||
| continuation.resume(throwing: NSError(domain: "ClerkExpo", code: 3, userInfo: [NSLocalizedDescriptionKey: "No root view controller available to present auth"])) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Double-resume crash if rootViewController is nil.
The VC is created (with its completion closure capturing the continuation) before checking whether rootViewController exists. If rootVC is nil, the continuation is resumed with an error on line 193. Then the VC is deallocated → its deinit cancels the event task → the stream ends → the post-loop code calls completion(.failure(...)) → second resume on the same continuation → crash.
Fix by checking for rootVC before creating the VC (same applies to presentUserProfile at lines 207–228):
Proposed fix
return try await withCheckedThrowingContinuation { continuation in
DispatchQueue.main.async {
+ guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else {
+ continuation.resume(throwing: NSError(domain: "ClerkExpo", code: 3, userInfo: [NSLocalizedDescriptionKey: "No root view controller available to present auth"]))
+ return
+ }
+
guard let vc = factory.createAuthViewController(mode: mode, dismissable: dismissable, completion: { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}) else {
continuation.resume(throwing: NSError(domain: "ClerkExpo", code: 2, userInfo: [NSLocalizedDescriptionKey: "Could not create auth view controller"]))
return
}
- if let rootVC = UIApplication.shared.keyWindow?.rootViewController {
- rootVC.present(vc, animated: true)
- } else {
- continuation.resume(throwing: NSError(domain: "ClerkExpo", code: 3, userInfo: [NSLocalizedDescriptionKey: "No root view controller available to present auth"]))
- }
+ rootVC.present(vc, animated: true)
}
}🤖 Prompt for AI Agents
In `@packages/expo/ios/ClerkExpoModule.swift` around lines 176 - 197, The code can
double-resume the continuation because factory.createAuthViewController is
called (and its completion captures the continuation) before verifying there is
a root view controller; if rootVC is nil you resume the continuation and the
VC’s deinit may later call the same continuation. Fix by checking for
UIApplication.shared.keyWindow?.rootViewController (or equivalent safe root)
before calling factory.createAuthViewController in the auth flow and in
presentUserProfile, returning/throwing the NSError immediately if no root
exists; only create and present the view controller (via
factory.createAuthViewController / the user profile factory method) after
confirming a non-nil rootVC so the VC’s completion cannot race to resume the
same continuation twice.
| public func createAuthView( | ||
| mode: String, | ||
| dismissable: Bool, | ||
| onEvent: @escaping (String, [String: Any]) -> Void | ||
| ) -> UIView? { | ||
| let authMode: AuthView.Mode | ||
| switch mode { | ||
| case "signIn": | ||
| authMode = .signIn | ||
| case "signUp": | ||
| authMode = .signUp | ||
| default: | ||
| authMode = .signInOrUp | ||
| } | ||
|
|
||
| let hostingController = UIHostingController( | ||
| rootView: ClerkInlineAuthWrapperView( | ||
| mode: authMode, | ||
| dismissable: dismissable, | ||
| onEvent: onEvent | ||
| ) | ||
| ) | ||
| hostingController.view.backgroundColor = .clear | ||
| return hostingController.view | ||
| } | ||
|
|
||
| public func createUserProfileView( | ||
| dismissable: Bool, | ||
| onEvent: @escaping (String, [String: Any]) -> Void | ||
| ) -> UIView? { | ||
| let hostingController = UIHostingController( | ||
| rootView: ClerkInlineProfileWrapperView( | ||
| dismissable: dismissable, | ||
| onEvent: onEvent | ||
| ) | ||
| ) | ||
| hostingController.view.backgroundColor = .clear | ||
| return hostingController.view | ||
| } |
There was a problem hiding this comment.
Return type mismatch with protocol — will not compile.
createAuthView and createUserProfileView return UIView? here, but ClerkViewFactoryProtocol (in ClerkExpoModule.swift lines 19–20) declares them as returning UIViewController?. This is a compile-time error — ClerkViewFactory does not conform to the protocol.
The callers in ClerkExpoModule.swift (lines 47, 109) also expect a UIViewController? and attach it as a child view controller. You need to return the UIHostingController itself, not its .view.
Proposed fix
public func createAuthView(
mode: String,
dismissable: Bool,
onEvent: `@escaping` (String, [String: Any]) -> Void
- ) -> UIView? {
+ ) -> UIViewController? {
let authMode: AuthView.Mode
switch mode {
case "signIn":
authMode = .signIn
case "signUp":
authMode = .signUp
default:
authMode = .signInOrUp
}
let hostingController = UIHostingController(
rootView: ClerkInlineAuthWrapperView(
mode: authMode,
dismissable: dismissable,
onEvent: onEvent
)
)
hostingController.view.backgroundColor = .clear
- return hostingController.view
+ return hostingController
}
public func createUserProfileView(
dismissable: Bool,
onEvent: `@escaping` (String, [String: Any]) -> Void
- ) -> UIView? {
+ ) -> UIViewController? {
let hostingController = UIHostingController(
rootView: ClerkInlineProfileWrapperView(
dismissable: dismissable,
onEvent: onEvent
)
)
hostingController.view.backgroundColor = .clear
- return hostingController.view
+ return hostingController
}🤖 Prompt for AI Agents
In `@packages/expo/ios/ClerkViewFactory.swift` around lines 87 - 125, The two
methods createAuthView and createUserProfileView currently return UIView? which
mismatches ClerkViewFactoryProtocol (expects UIViewController?); change their
return types to UIViewController? and return the UIHostingController instance
(hostingController) instead of hostingController.view, keeping any view
configuration (e.g., hostingController.view.backgroundColor = .clear) intact so
callers in ClerkExpoModule can attach the view controller as a child.
| deinit { | ||
| authEventTask?.cancel() | ||
| } | ||
|
|
||
| private func subscribeToAuthEvents() { | ||
| authEventTask = Task { @MainActor [weak self] in | ||
| for await event in Clerk.shared.authEventEmitter.events { | ||
| guard let self = self else { return } | ||
| switch event { | ||
| case .signInCompleted(let signIn): | ||
| if let sessionId = signIn.createdSessionId { | ||
| self.completion(.success(["sessionId": sessionId, "type": "signIn"])) | ||
| self.dismiss(animated: true) | ||
| } else { | ||
| self.completion(.failure(NSError(domain: "ClerkExpo", code: 4, userInfo: [NSLocalizedDescriptionKey: "Sign-in completed but no session was created"]))) | ||
| self.dismiss(animated: true) | ||
| } | ||
| case .signUpCompleted(let signUp): | ||
| if let sessionId = signUp.createdSessionId { | ||
| self.completion(.success(["sessionId": sessionId, "type": "signUp"])) | ||
| self.dismiss(animated: true) | ||
| } else { | ||
| self.completion(.failure(NSError(domain: "ClerkExpo", code: 4, userInfo: [NSLocalizedDescriptionKey: "Sign-up completed but no session was created"]))) | ||
| self.dismiss(animated: true) | ||
| } | ||
| default: | ||
| break | ||
| } | ||
| } | ||
| // Stream ended without a completion event | ||
| guard let self = self else { return } | ||
| self.completion(.failure(NSError(domain: "ClerkExpo", code: 5, userInfo: [NSLocalizedDescriptionKey: "Auth event stream ended unexpectedly"]))) | ||
| } | ||
| } |
There was a problem hiding this comment.
Continuation leaked when modal is dismissed without completing auth/profile flow.
If the user dismisses the modal (e.g., via a dismiss button in the AuthView when dismissable: true), the VC gets deallocated → deinit cancels the task → for await loop ends → guard let self = self else { return } on line 226/276 bails because self is already nil → completion is never called → the withCheckedThrowingContinuation in ClerkExpoModule is never resumed, which traps at runtime.
The fix from the prior review added post-loop completion calls, but they're unreachable in the deinit path because self is nil. Capture completion directly in the task closure instead of going through self:
Proposed fix (auth wrapper; apply same to profile wrapper)
private func subscribeToAuthEvents() {
+ let completion = self.completion
- authEventTask = Task { `@MainActor` [weak self] in
+ authEventTask = Task { `@MainActor` [weak self] in
for await event in Clerk.shared.authEventEmitter.events {
guard let self = self else { return }
switch event {
case .signInCompleted(let signIn):
...
case .signUpCompleted(let signUp):
...
default:
break
}
}
// Stream ended without a completion event
- guard let self = self else { return }
- self.completion(.failure(NSError(domain: "ClerkExpo", code: 5, userInfo: [NSLocalizedDescriptionKey: "Auth event stream ended unexpectedly"])))
+ completion(.failure(NSError(domain: "ClerkExpo", code: 5, userInfo: [NSLocalizedDescriptionKey: "Auth event stream ended unexpectedly"])))
}
}Also applies to: 259-279
🤖 Prompt for AI Agents
In `@packages/expo/ios/ClerkViewFactory.swift` around lines 196 - 229, The task
can cancel when the VC is deallocated which makes `self` nil and prevents
calling `completion`, leaving the awaiting continuation trapped; fix by
capturing the VC's completion closure by value before creating the Task (e.g.
let capturedCompletion = self.completion), then use that capturedCompletion
inside subscribeToAuthEvents' Task instead of going through self so the
failure/resolution path after the for-await and on cancellation will always call
the continuation; apply the same change to the profile event wrapper (the
analogous subscribeToProfileEvents/profileEventTask block).
| // CRITICAL: Must call load() after configure() to restore session from keychain | ||
| do { | ||
| try await Clerk.shared.load() | ||
| print("✅ [ClerkViewFactory] Clerk load() completed") | ||
| } catch { | ||
| print("❌ [ClerkViewFactory] Clerk load() failed: \(error)") | ||
| } |
There was a problem hiding this comment.
configure() swallows load() failure — callers will believe initialization succeeded.
The function is declared async throws but the catch on Line 34 prints the error and continues. Downstream code (and the JS SDK) will assume Clerk is ready when it isn't, leading to broken auth flows.
Either re-throw or propagate the error:
Proposed fix
do {
try await Clerk.shared.load()
print("✅ [ClerkViewFactory] Clerk load() completed")
} catch {
print("❌ [ClerkViewFactory] Clerk load() failed: \(error)")
+ throw error
}🤖 Prompt for AI Agents
In `@packages/expo/ios/templates/ClerkViewFactory.swift` around lines 29 - 35, The
configure() function currently swallows errors from Clerk.shared.load() by
catching and only printing them, so change it to propagate the failure instead
of continuing: remove the do/catch that swallows the error (or rethrow inside
the catch) and call try await Clerk.shared.load() directly (or rethrow the
caught error) so the async throws signature of configure() surfaces load()
failures to callers; keep the logging around the successful path but ensure any
load() error is rethrown to the caller.
| userDict["primaryEmailAddress"] = firstEmail.emailAddress | ||
| } | ||
| result["user"] = userDict | ||
| NSLog("✅ [ClerkViewFactory] User found: \(user.firstName ?? "N/A") \(user.lastName ?? "")") |
There was a problem hiding this comment.
PII logged in production — user names written to device logs via NSLog.
Line 163 logs user.firstName and user.lastName through NSLog, which persists to the system log and is accessible via device console/bug reports. This should be removed or gated behind a debug check.
🤖 Prompt for AI Agents
In `@packages/expo/ios/templates/ClerkViewFactory.swift` at line 163, NSLog in
ClerkViewFactory.swift is writing PII (user.firstName and user.lastName) to
device logs; remove the direct name logging or gate it so it never logs names in
production. Replace the NSLog call that references user.firstName/user.lastName
in the ClerkViewFactory codepath with either (a) no log of names, (b) a log
guarded by a debug-only build flag (`#if` DEBUG) or a runtime canLogPII check, or
(c) log non-PII information (e.g., anonymized user id or status) instead. Ensure
the change targets the NSLog call that references user.firstName and
user.lastName so production builds do not emit PII.
| .catch(err => { | ||
| if (__DEV__) { | ||
| console.error('Clerk: Failed to load:', err); | ||
| } | ||
| this.#eventBus.emit(clerkEvents.Status, 'error'); | ||
| this.emitLoaded(); | ||
| }); |
There was a problem hiding this comment.
emitLoaded() in the error path will resolve #waitForClerkJS() with a null clerkjs, causing NPEs.
When clerk.load() rejects, this.clerkjs is never assigned (remains null) and #initialized stays false. However, emitLoaded() on line 337 fires any pending loadedListeners, including those registered by #waitForClerkJS() (line 506), which resolves with this.clerkjs! — a null value. Callers like authenticateWithGoogleOneTap, __internal_loadStripeJs, and __internal_updateProps will then immediately dereference null and throw.
Compare with the getEntryChunks error path (lines 542–547), which does not call emitLoaded() and simply returns.
Either skip emitLoaded() in the error path (let the promise hang, matching existing behavior) or guard #waitForClerkJS against a null resolution.
Suggested fix — don't resolve loaded listeners on failure
.catch(err => {
if (__DEV__) {
console.error('Clerk: Failed to load:', err);
}
this.#eventBus.emit(clerkEvents.Status, 'error');
- this.emitLoaded();
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .catch(err => { | |
| if (__DEV__) { | |
| console.error('Clerk: Failed to load:', err); | |
| } | |
| this.#eventBus.emit(clerkEvents.Status, 'error'); | |
| this.emitLoaded(); | |
| }); | |
| .catch(err => { | |
| if (__DEV__) { | |
| console.error('Clerk: Failed to load:', err); | |
| } | |
| this.#eventBus.emit(clerkEvents.Status, 'error'); | |
| }); |
🤖 Prompt for AI Agents
In `@packages/react/src/isomorphicClerk.ts` around lines 332 - 338, The catch
block for clerk.load currently calls this.emitLoaded(), which resolves
`#waitForClerkJS`() with a null this.clerkjs and causes NPEs; update the catch in
isomorphicClerk.ts (the clerk.load().catch handler) to NOT call
this.emitLoaded() on failure—only emit the error status
(this.#eventBus.emit(clerkEvents.Status, 'error')) and return so loaded
listeners are not resolved with null; alternatively, if you prefer preserving
resolution, change `#waitForClerkJS` to reject/throw when this.clerkjs is null,
but prefer removing the emitLoaded() call in the clerk.load error path
referenced by emitLoaded, `#waitForClerkJS`, clerk.load, this.clerkjs, and
`#initialized`.
@clerk/agent-toolkit
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/dev-cli
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
Description
These changes can be tested by using the snapshot that will be commented in this PR discussion, and installing that into the expo quickstart repo on the branch:
chris/mobile-343-bridge-android-to-a-native-module-that-is-available-in-the(same branch name as here).
https://linear.app/clerk/issue/MOBILE-342/bridge-ios-to-a-native-module-that-is-available-in-the-expo-sdk
MOBILE-289
https://linear.app/clerk/issue/MOBILE-289/expo-google-universal-sign-in
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change
Summary by CodeRabbit
New Features
Bug Fixes
Documentation