Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/fair-expo-kotlin-fixture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
6 changes: 6 additions & 0 deletions integration/templates/expo-kotlin-alignment/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/.expo/
/.env*
/android/
/ios/
/node_modules/
/pnpm-lock.yaml
75 changes: 75 additions & 0 deletions integration/templates/expo-kotlin-alignment/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';
import { ClerkProvider, useAuth, useClerk } from '@clerk/expo';
import { AuthView, UserProfileView } from '@clerk/expo/native';
import { tokenCache } from '@clerk/expo/token-cache';

function Probe() {
const { isLoaded, isSignedIn, getToken } = useAuth();
const { signOut } = useClerk();
const [message, setMessage] = useState('');
const [profile, setProfile] = useState(false);
const [mounted, setMounted] = useState(true);

async function checkToken() {
try {
setMessage((await getToken()) ? 'Session token obtained' : 'No session token');
} catch (error) {
setMessage(error.message);
}
}

return (
<View style={{ flex: 1, paddingTop: 50, paddingBottom: 35 }}>
<Text>Clerk Kotlin alignment probe</Text>
<Text>
Loaded: {String(isLoaded)}; signed in: {String(isSignedIn)}
</Text>
<Text>{message}</Text>
<Button
title={mounted ? 'Unmount native view' : 'Mount native view'}
onPress={() => setMounted(!mounted)}
/>
{isSignedIn && (
<>
<Button
title='Check session token'
onPress={checkToken}
/>
<Button
title='Toggle profile'
onPress={() => setProfile(!profile)}
/>
<Button
title='Sign out'
onPress={async () => {
await signOut();
setMessage('Signed out');
}}
/>
</>
)}
{mounted &&
(isSignedIn ? profile ? <UserProfileView /> : <Text>Authenticated</Text> : <AuthView mode='signInOrUp' />)}
</View>
);
}

export default function App() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
if (!publishableKey?.startsWith('pk_test_')) {
return (
<View style={{ padding: 40 }}>
<Text>Set a development EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY and rebuild.</Text>
</View>
);
}
return (
<ClerkProvider
publishableKey={publishableKey}
tokenCache={tokenCache}
>
<Probe />
</ClerkProvider>
);
}
128 changes: 128 additions & 0 deletions integration/templates/expo-kotlin-alignment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Expo 58 Kotlin alignment experiment

This isolated Android fixture explores a fully aligned Kotlin toolchain without
changing Clerk's shipped defaults. It is pinned to published `@clerk/expo@4.7.1`,
not the current monorepo package, so the experiment remains independent of changes
to Clerk's native dependencies and package split.

It complements [#9791](https://github.com/clerk/javascript/pull/9791), which addresses
the immediate release-build failure, and
[#9955](https://github.com/clerk/javascript/pull/9955), which makes native components
optional. [#9974](https://github.com/clerk/javascript/pull/9974) separately enables
optimization in the existing Android integration workflow.

## Configuration

| Input | Version |
| ---------------------------------- | ------------------------------------------- |
| Expo | 58.0.0-preview.8 |
| React Native | 0.88.0-rc.2 |
| React / React DOM | 19.3.0 |
| Clerk Expo | 4.7.1 |
| Clerk native API / UI | 1.1.8, selected by the pinned Clerk package |
| Kotlin / resolved Compose compiler | 2.4.20 |
| SavedState Compose | 1.3.3 |

The official `expo-build-properties` plugin selects Kotlin 2.4.20 and enables
minification and resource shrinking. The fixture-local pnpm patch adds SavedState
Compose, removes six stdlib exclusions, and removes the local, cross-project and
config-plugin metadata-check bypasses. It does not add a `dontwarn` rule.

The dependency reports from the original experiment resolved both the Kotlin
compiler and Compose compiler to 2.4.20 even though Clerk's plugin declaration still
says 2.1.20. No additional Compose patch was necessary on this exact stack.

Only this fixture consumes the patch. It is not an installation recommendation for
older Expo SDKs and does not change Clerk's declared Expo support range. The
expected Expo peer-range warning is retained rather than hidden.

## Run

Use Node 24.15 or newer, pnpm, JDK 17 and an Android SDK. Start an arm64 emulator;
this fixture intentionally builds only `arm64-v8a` to match the original test.

From this directory:

```sh
pnpm install
```

Create an ignored `.env` with your own development-instance publishable key:

```dotenv
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_REPLACE_WITH_YOUR_DEVELOPMENT_KEY
```

```sh
pnpm android --device YOUR_EMULATOR_NAME
```

The publication fixture also passed a fresh pnpm install and optimized build.
Expo CLI subsequently tried to open a development-client URI that this standalone
release does not handle; the build and installation succeeded. If that occurs,
open the installed app from the emulator launcher.

Expo manages prebuild and compilation. After changing native configuration, run
`pnpm prebuild:android` before building again. The fixture refuses a production
publishable key. No secret key or test credentials belong in this directory.

Confirm that `android/app/build/outputs/mapping/release/mapping.txt` is nonempty.
Inspect the generated merged R8 configuration to confirm no
`-dontwarn kotlin.MustUseReturnValues` rule was added, and inspect the patched
Clerk Gradle/plugin files and generated app Gradle file for absent
`-Xskip-metadata-version-check` flags. Dependency reports should confirm the actual
compiler artifacts, not only the configuration property.

## Manual authentication checks

Use an account in the development instance:

1. Sign in through the native form. Complete any verification challenge.
2. Select **Check session token**; the app reports presence, never the token.
3. Force-stop and reopen the app; check that the session and token remain available.
4. Open the profile, unmount/remount it, and confirm it renders again.
5. Sign out and confirm the signed-out form returns.

The original isolated alignment build passed these checks on an API 36 arm64
emulator. Independent development Backend API reads confirmed the session changed
from active to removed. The development test account was deleted afterwards.
The publication fixture removes the original probe's test-user deletion control;
manage any account cleanup separately in your development instance.

## Comparison evidence

These are isolated reproduction results against published Clerk 4.7.1, not a full
execution of #9791 or a certification of all patch releases and auth providers.
Both small-fix variants kept minification and resource shrinking enabled.

| Expo | React Native | Stock | Remove exclusions | Exact annotation rule |
| ---------------- | ------------ | ------------------------------------------- | ----------------- | --------------------- |
| 54.0.37 | 0.81.5 | Missing annotation | Build/auth pass | Build/auth pass |
| 55.0.31 | 0.83.10 | Missing annotation | Build/auth pass | Build/auth pass |
| 56.0.23 | 0.85.3 | Missing annotation | Build/auth pass | Build/auth pass |
| 57.0.25 | 0.86.3 | Missing annotation | Build/auth pass | Build/auth pass |
| 58.0.0-preview.8 | 0.88.0-rc.2 | SavedState failure, then missing annotation | Build/auth pass | Build/auth pass |

The Expo 54 narrow/runtime checks used native Clerk 1.1.9; the 55–58 checks used
1.1.8. On 58, both small fixes included SavedState Compose 1.3.3; the 1.4.0 addition
from #9791 was separately build/startup-tested on 57. The 58 consumer-rule build
produced identical DEX to the authenticated app-level-rule APK, and merged R8
configuration confirmed delivery from Clerk's library.

Older SDKs emitted metadata warnings with both small fixes. The 55 narrow build
completed R8 with 353 warnings before a disk-full interruption during lint; its
successful unchanged retry reused R8 outputs and must not be called warning-free.
The fully aligned 58 experiment had no R8 metadata warnings.

## Boundaries and next decision

The native publication floor, older Expo compiler constraints and supported
version policy still need maintainer direction. This fixture is deliberately not
a conditional production implementation based on SDK version.

Physical devices, Google/Apple OAuth, passkeys, long-duration token expiry,
existing-install session migration and the full upstream CI suite are outside
this experiment's completed coverage. The package versions are pinned directly;
transitive JavaScript dependencies are resolved at install time, following the
existing standalone fixture convention. Record the generated lockfile when
comparing a later dependency graph.
24 changes: 24 additions & 0 deletions integration/templates/expo-kotlin-alignment/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"expo": {
"name": "Clerk Kotlin Alignment",
"slug": "clerk-kotlin-alignment",
"version": "1.0.0",
"android": {
"package": "com.clerk.expo.kotlin_alignment"
},
"plugins": [
"@clerk/expo",
[
"expo-build-properties",
{
"android": {
"enableMinifyInReleaseBuilds": true,
"enableShrinkResourcesInReleaseBuilds": true,
"buildArchs": ["arm64-v8a"],
"kotlinVersion": "2.4.20"
}
}
]
]
}
}
4 changes: 4 additions & 0 deletions integration/templates/expo-kotlin-alignment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { registerRootComponent } from 'expo';
import App from './App';

registerRootComponent(App);
25 changes: 25 additions & 0 deletions integration/templates/expo-kotlin-alignment/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "clerk-expo-kotlin-alignment-fixture",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"android": "expo run:android --variant release",
"prebuild:android": "expo prebuild --clean --platform android"
},
"dependencies": {
"@clerk/expo": "4.7.1",
"expo": "58.0.0-preview.8",
"expo-auth-session": "58.0.5",
"expo-build-properties": "58.0.8",
"expo-constants": "58.0.8",
"expo-crypto": "58.0.2",
"expo-secure-store": "58.0.0",
"expo-status-bar": "58.0.1",
"expo-web-browser": "58.0.2",
"react": "19.3.0",
"react-dom": "19.3.0",
"react-native": "0.88.0-rc.2"
},
"packageManager": "pnpm@10.34.5"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
diff --git a/android/build.gradle b/android/build.gradle
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -59,10 +59,6 @@

kotlinOptions {
jvmTarget = "17"
- // TODO: Remove once clerk-android pins its transitive deps to a Kotlin version compatible with Expo.
- // clerk-android transitive deps (e.g. kotlinx-serialization-core:1.10.0) ship Kotlin 2.3.x metadata,
- // but Expo SDK 54/55 uses Kotlin 2.1.x. This flag lets the 2.1.x compiler read 2.3.x metadata safely.
- freeCompilerArgs += ['-Xskip-metadata-version-check']
}

buildFeatures {
@@ -83,25 +79,9 @@
}
}

-// clerk-android is compiled with Kotlin 2.3.x, but Expo/RN projects typically use an older
-// Kotlin version (e.g. 2.1.x). This causes "incompatible version of Kotlin" metadata errors
-// at :app:compileDebugKotlin. Applying -Xskip-metadata-version-check allows the older
-// compiler to read 2.3.x metadata without failing.
-try {
- rootProject.allprojects {
- tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
- compilerOptions {
- freeCompilerArgs.add('-Xskip-metadata-version-check')
- }
- }
- }
-} catch (Exception ignored) {
- // Future Gradle versions with Isolated Projects may block cross-project configuration.
- // In that case, users should add '@clerk/expo' to their app.json plugins array instead,
- // which applies the flag via the config plugin's withClerkAndroid.
-}

dependencies {
+ implementation "androidx.savedstate:savedstate-compose:1.3.3"
testImplementation "junit:junit:4.13.2"

implementation project(':expo-modules-core')
@@ -110,12 +90,8 @@
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion"

// Clerk Android SDK with prebuilt UI
- // Exclude kotlin-stdlib to prevent 2.3.0 from polluting the project
// Exclude transitive OkHttp modules so the compatible versions below control resolution.
implementation("com.clerk:clerk-android-ui:$clerkAndroidUiVersion") {
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
exclude group: 'com.squareup.okhttp3', module: 'okhttp-urlconnection'
}
@@ -123,9 +99,6 @@
// clerk-android-api. Pinning the api explicitly here keeps consumers
// compiling against the same version we ship the UI from.
implementation("com.clerk:clerk-android-api:$clerkAndroidApiVersion") {
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
- exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
exclude group: 'com.squareup.okhttp3', module: 'okhttp-urlconnection'
}
diff --git a/app.plugin.js b/app.plugin.js
--- a/app.plugin.js
+++ b/app.plugin.js
@@ -159,26 +159,6 @@
}
}
console.log('✅ Clerk Android packaging exclusions added');
- }
-
- // --- Kotlin metadata version check skip ---
- if (!buildGradle.includes('-Xskip-metadata-version-check')) {
- const kotlinOptionsMatch = buildGradle.match(/kotlinOptions\s*\{/);
- if (kotlinOptionsMatch) {
- buildGradle = buildGradle.replace(
- /kotlinOptions\s*\{/,
- `kotlinOptions {\n // Clerk: allow reading metadata from newer Kotlin versions\n freeCompilerArgs += ['-Xskip-metadata-version-check']`,
- );
- } else {
- const androidMatch = buildGradle.match(/android\s*\{/);
- if (androidMatch) {
- buildGradle = buildGradle.replace(
- /android\s*\{/,
- `android {\n kotlinOptions {\n // Clerk: allow reading metadata from newer Kotlin versions\n freeCompilerArgs += ['-Xskip-metadata-version-check']\n }`,
- );
- }
- }
- console.log('✅ Clerk Android Kotlin metadata version check skip added');
}

modConfig.modResults.contents = buildGradle;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
packages:
- '.'

patchedDependencies:
'@clerk/expo@4.7.1': patches/clerk-expo.patch