diff --git a/.changeset/expo-native-components-package.md b/.changeset/expo-native-components-package.md new file mode 100644 index 00000000000..88789036ac9 --- /dev/null +++ b/.changeset/expo-native-components-package.md @@ -0,0 +1,16 @@ +--- +'@clerk/expo': minor +'@clerk/expo-native-components': minor +--- + +Introduce `@clerk/expo-native-components`, a new package that the native components (`AuthView`, `UserButton`, `UserProfileView`) now require. Install it and rebuild your app. + +```sh +npx expo install @clerk/expo-native-components +``` + +If it's missing, rendering a native component throws an error with these same steps. Install the package before you ship this version in an over-the-air update, because the error is thrown at runtime and not at build time. + +Import the native components and their hooks from `@clerk/expo/native-components`. `@clerk/expo/native` is deprecated. It still works and logs a warning in development. + +Apps that don't install the package no longer include the Clerk iOS and Android SDKs and no longer require iOS 17. On Android, those apps also stop migrating biometric credentials enrolled with earlier versions of `@clerk/expo`, so users who weren't migrated by an earlier release need to enroll again. diff --git a/.claude/skills/clerk-monorepo/references/package-map.md b/.claude/skills/clerk-monorepo/references/package-map.md index c5cabf3765b..47abcecfa41 100644 --- a/.claude/skills/clerk-monorepo/references/package-map.md +++ b/.claude/skills/clerk-monorepo/references/package-map.md @@ -16,32 +16,33 @@ browser directly), **adapter** (framework SDK), **ui/i18n**, **tooling**. The "B two packages whose runtime is pushed into apps pinned to older SDKs, so they carry the strict backwards-compatibility contract (see `breaking-changes.md`). -| Package | Category | BC | Purpose | -| ----------------------------- | --------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `@clerk/shared` | foundational | | Internal utilities used by all SDKs (storage, events, React helpers). Hosts the shared types as `@clerk/shared/types`. Most-depended-on package. | -| `@clerk/backend` | foundational | | Backend API REST client, JWT verification, webhook helpers. Used by every server adapter. | -| `@clerk/clerk-js` | browser-runtime | ⚠️ | The browser runtime (script tag). Backwards-compat sensitive. | -| `@clerk/ui` | ui | ⚠️ | React components for the hosted sign-in / sign-up flows (`packages/ui/src/components`). Consumed by the react/astro/vue/chrome-extension adapters. Backwards-compat sensitive. | -| `@clerk/mosaic` | ui | | Experimental next-generation React components. Public ESM package (`UserButton` + `styles.css`). Ships unstyled, accessible primitives (dialog, menu, popover, ...) internally under `src/primitives/`. Reads Clerk context from the host SDK via `@clerk/shared`. | -| `@clerk/react` | adapter (core) | | React hooks and context (`useAuth`, `useUser`, `useOrganization`, ...). Shared by the React-based adapters. | -| `@clerk/nextjs` | adapter | | Next.js SDK: middleware, route handlers, server components. | -| `@clerk/express` | adapter | | Express middleware and server helpers. | -| `@clerk/fastify` | adapter | | Fastify plugin. | -| `@clerk/hono` | adapter | | Hono SDK (edge / serverless). | -| `@clerk/astro` | adapter | | Astro integration (components + server utilities). | -| `@clerk/nuxt` | adapter | | Nuxt module (Vue). | -| `@clerk/vue` | adapter | | Vue 3 composables and components. | -| `@clerk/react-router` | adapter | | React Router v7 SDK. | -| `@clerk/tanstack-react-start` | adapter | | TanStack React Start SDK. | -| `@clerk/expo` | adapter | | React Native / Expo SDK. | -| `@clerk/expo-passkeys` | adapter | | Passkeys companion library for Expo. | -| `@clerk/chrome-extension` | browser-runtime | | SDK for Chrome extension contexts. | -| `@clerk/localizations` | ui/i18n | | Translation strings for the UI components. Consumed by `ui`. | -| `@clerk/testing` | tooling | | E2E test helpers for consumers (Playwright + Cypress). | -| `@clerk/msw` | tooling | | MSW request handlers for mocking the Clerk API in tests. Private (not published). | -| `@clerk/swingset` | tooling | | Component explorer for `@clerk/mosaic`. Private (not published). | -| `@clerk/upgrade` | tooling | | CLI codemod tool for upgrading consumers between SDK versions. | -| `@clerk/eslint-plugin` | tooling | | ESLint plugin enforcing Clerk patterns across JavaScript frameworks (lint rules shipped to apps). Published. | +| Package | Category | BC | Purpose | +| ------------------------------- | --------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `@clerk/shared` | foundational | | Internal utilities used by all SDKs (storage, events, React helpers). Hosts the shared types as `@clerk/shared/types`. Most-depended-on package. | +| `@clerk/backend` | foundational | | Backend API REST client, JWT verification, webhook helpers. Used by every server adapter. | +| `@clerk/clerk-js` | browser-runtime | ⚠️ | The browser runtime (script tag). Backwards-compat sensitive. | +| `@clerk/ui` | ui | ⚠️ | React components for the hosted sign-in / sign-up flows (`packages/ui/src/components`). Consumed by the react/astro/vue/chrome-extension adapters. Backwards-compat sensitive. | +| `@clerk/mosaic` | ui | | Experimental next-generation React components. Public ESM package (`UserButton` + `styles.css`). Ships unstyled, accessible primitives (dialog, menu, popover, ...) internally under `src/primitives/`. Reads Clerk context from the host SDK via `@clerk/shared`. | +| `@clerk/react` | adapter (core) | | React hooks and context (`useAuth`, `useUser`, `useOrganization`, ...). Shared by the React-based adapters. | +| `@clerk/nextjs` | adapter | | Next.js SDK: middleware, route handlers, server components. | +| `@clerk/express` | adapter | | Express middleware and server helpers. | +| `@clerk/fastify` | adapter | | Fastify plugin. | +| `@clerk/hono` | adapter | | Hono SDK (edge / serverless). | +| `@clerk/astro` | adapter | | Astro integration (components + server utilities). | +| `@clerk/nuxt` | adapter | | Nuxt module (Vue). | +| `@clerk/vue` | adapter | | Vue 3 composables and components. | +| `@clerk/react-router` | adapter | | React Router v7 SDK. | +| `@clerk/tanstack-react-start` | adapter | | TanStack React Start SDK. | +| `@clerk/expo` | adapter | | React Native / Expo SDK. | +| `@clerk/expo-native-components` | adapter | | Native components (`AuthView`, `UserButton`, `UserProfileView`) and the native module for Expo, built on clerk-ios / clerk-android. Optional companion to `@clerk/expo`. | +| `@clerk/expo-passkeys` | adapter | | Passkeys companion library for Expo. | +| `@clerk/chrome-extension` | browser-runtime | | SDK for Chrome extension contexts. | +| `@clerk/localizations` | ui/i18n | | Translation strings for the UI components. Consumed by `ui`. | +| `@clerk/testing` | tooling | | E2E test helpers for consumers (Playwright + Cypress). | +| `@clerk/msw` | tooling | | MSW request handlers for mocking the Clerk API in tests. Private (not published). | +| `@clerk/swingset` | tooling | | Component explorer for `@clerk/mosaic`. Private (not published). | +| `@clerk/upgrade` | tooling | | CLI codemod tool for upgrading consumers between SDK versions. | +| `@clerk/eslint-plugin` | tooling | | ESLint plugin enforcing Clerk patterns across JavaScript frameworks (lint rules shipped to apps). Published. | Tests: `pnpm turbo test --filter=@clerk/` (or `pnpm --filter @clerk/ test` after a build). Most packages use vitest; `@clerk/backend` runs a multi-runtime suite (node + edge + diff --git a/.github/labeler.yml b/.github/labeler.yml index b6f18eeb545..d07fbb87c3f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -28,6 +28,10 @@ expo: - changed-files: - any-glob-to-any-file: packages/expo/** +expo-native-components: + - changed-files: + - any-glob-to-any-file: packages/expo-native-components/** + express: - changed-files: - any-glob-to-any-file: packages/express/** diff --git a/.github/workflows/api-changes.yml b/.github/workflows/api-changes.yml index 25971a1ad1b..9feadb28f55 100644 --- a/.github/workflows/api-changes.yml +++ b/.github/workflows/api-changes.yml @@ -19,6 +19,7 @@ on: - 'packages/clerk-js/**' - 'packages/expo/**' - 'packages/expo-google-signin/**' + - 'packages/expo-native-components/**' - 'packages/expo-passkeys/**' - 'packages/express/**' - 'packages/fastify/**' @@ -56,6 +57,7 @@ env: --filter=@clerk/clerk-js --filter=@clerk/expo --filter=@clerk/expo-google-signin + --filter=@clerk/expo-native-components --filter=@clerk/expo-passkeys --filter=@clerk/express --filter=@clerk/fastify diff --git a/.github/workflows/expo-native-build.yml b/.github/workflows/expo-native-build.yml index baba13fb3cd..7b4731cc6fe 100644 --- a/.github/workflows/expo-native-build.yml +++ b/.github/workflows/expo-native-build.yml @@ -13,6 +13,7 @@ on: - 'packages/expo/**' - 'packages/expo-biometrics/**' - 'packages/expo-google-signin/**' + - 'packages/expo-native-components/**' workflow_dispatch: permissions: @@ -88,6 +89,7 @@ jobs: packages/expo \ packages/expo-biometrics \ packages/expo-google-signin \ + packages/expo-native-components \ packages/react \ packages/shared \ "$FIXTURE_DIR" | @@ -128,6 +130,7 @@ jobs: mkdir -p "$SDK_PACK_DIR" pnpm --filter @clerk/expo pack --pack-destination "$SDK_PACK_DIR" pnpm --filter @clerk/expo-google-signin pack --pack-destination "$SDK_PACK_DIR" + pnpm --filter @clerk/expo-native-components pack --pack-destination "$SDK_PACK_DIR" pnpm --filter @clerk/expo-biometrics pack --pack-destination "$SDK_PACK_DIR" - name: Install fixture dependencies @@ -139,11 +142,12 @@ jobs: run: | cp "package.sdk-$EXPO_SDK.json" package.json pnpm install --no-frozen-lockfile - # [0-9] keeps this glob off the clerk-expo-google-signin tarball. + # [0-9] keeps this glob off the other clerk-expo-* tarballs. SDK_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-[0-9]*.tgz)" GOOGLE_SIGNIN_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-google-signin-*.tgz)" + NATIVE_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-native-components-*.tgz)" BIOMETRICS_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-biometrics-*.tgz)" - pnpm add "$SDK_TARBALL" "$GOOGLE_SIGNIN_TARBALL" "$BIOMETRICS_TARBALL" -w + pnpm add "$SDK_TARBALL" "$GOOGLE_SIGNIN_TARBALL" "$NATIVE_TARBALL" "$BIOMETRICS_TARBALL" -w # expo-dev-client makes even release builds boot into the dev # launcher (unreachable Metro in CI), which stalls every e2e # flow on a blank screen. Skip it on e2e jobs only. diff --git a/break-check.config.json b/break-check.config.json index 44a7de9fa25..c3313b14ada 100644 --- a/break-check.config.json +++ b/break-check.config.json @@ -5,6 +5,7 @@ "packages/chrome-extension", "packages/clerk-js", "packages/expo", + "packages/expo-native-components", "packages/expo-passkeys", "packages/express", "packages/fastify", diff --git a/integration/templates/expo-native/App.tsx b/integration/templates/expo-native/App.tsx index bfac480af41..86967b97b52 100644 --- a/integration/templates/expo-native/App.tsx +++ b/integration/templates/expo-native/App.tsx @@ -1,5 +1,5 @@ import { ClerkProvider, useAuth, useUser } from '@clerk/expo'; -import { AuthView, UserButton, UserProfileView } from '@clerk/expo/native'; +import { AuthView, UserButton, UserProfileView } from '@clerk/expo/native-components'; import { tokenCache } from '@clerk/expo/token-cache'; import { useState } from 'react'; import { Button, Modal, StyleSheet, Text, View } from 'react-native'; diff --git a/integration/templates/expo-native/app.json b/integration/templates/expo-native/app.json index 58e258ea523..d872ce8073b 100644 --- a/integration/templates/expo-native/app.json +++ b/integration/templates/expo-native/app.json @@ -12,6 +12,6 @@ "android": { "package": "com.clerk.exponativebuildfixture" }, - "plugins": ["expo-secure-store", "@clerk/expo", "expo-web-browser"] + "plugins": ["expo-secure-store", "@clerk/expo", "@clerk/expo-native-components", "expo-web-browser"] } } diff --git a/package.json b/package.json index c7c706f5e61..7d177785a6b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "changeset": "changeset", "changeset:empty": "pnpm changeset --empty", "clean": "turbo run clean", - "dev": "TURBO_UI=0 FORCE_COLOR=1 turbo dev --filter=@clerk/* --filter=!@clerk/expo --filter=!@clerk/tanstack-react-start --filter=!@clerk/chrome-extension", + "dev": "TURBO_UI=0 FORCE_COLOR=1 turbo dev --filter=@clerk/* --filter=!@clerk/expo --filter=!@clerk/expo-native-components --filter=!@clerk/tanstack-react-start --filter=!@clerk/chrome-extension", "dev:fe-libs": "TURBO_UI=0 FORCE_COLOR=1 turbo dev --filter=@clerk/clerk-js --filter=@clerk/ui --filter=@clerk/shared", "dev:js": "TURBO_UI=0 FORCE_COLOR=1 turbo dev:current --filter=@clerk/clerk-js", "dev:sandbox": "TURBO_UI=0 FORCE_COLOR=1 turbo dev:sandbox:serve", diff --git a/packages/expo/.gitignore b/packages/expo-native-components/.gitignore similarity index 100% rename from packages/expo/.gitignore rename to packages/expo-native-components/.gitignore diff --git a/packages/expo-native-components/LICENSE b/packages/expo-native-components/LICENSE new file mode 100644 index 00000000000..66914b6af7c --- /dev/null +++ b/packages/expo-native-components/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Clerk, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/expo-native-components/README.md b/packages/expo-native-components/README.md new file mode 100644 index 00000000000..1d400cb0443 --- /dev/null +++ b/packages/expo-native-components/README.md @@ -0,0 +1,77 @@ +

+ + + + + + +
+

@clerk/expo-native-components

+

+ +
+ +[![Chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) +[![Clerk documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs?utm_source=github&utm_medium=clerk_expo_native_components) +[![Follow on X](https://img.shields.io/twitter/follow/clerk?style=social)](https://x.com/intent/follow?screen_name=clerk) + +[Changelog](https://github.com/clerk/javascript/blob/main/packages/expo-native-components/CHANGELOG.md) +· +[Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml) +· +[Request a Feature](https://feedback.clerk.com/roadmap) +· +[Get help](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_expo_native_components) + +
+ +> [!NOTE] +> Install this package to use `AuthView`, `UserButton`, and `UserProfileView` from `@clerk/expo/native-components`. Its JavaScript API is used by `@clerk/expo` and isn't meant to be imported directly. + +## Getting Started + +`@clerk/expo-native-components` contains the native code behind Clerk's prebuilt native components for Expo apps that use [`@clerk/expo`](https://github.com/clerk/javascript/tree/main/packages/expo). The components are built on the Clerk iOS and Android SDKs. Apps that don't install it don't include those SDKs. + +### Prerequisites + +- `@clerk/expo` +- Expo 54 or later, in a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (the components are not available in Expo Go) +- iOS 17 or later + +### Installation + +```sh +npx expo install @clerk/expo-native-components +``` + +Then rebuild your native app. + +## Usage + +For further information, guides, and examples visit the [Expo reference documentation](https://clerk.com/docs/references/expo/overview?utm_source=github&utm_medium=clerk_expo_native_components). + +## Support + +For help, visit our [support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_expo_native_components). + +## Community + +Join our [Discord community](https://clerk.com/discord) to connect with other developers. + +## Contributing + +We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/clerk/javascript/blob/main/docs/CODE_OF_CONDUCT.md). + +## Security + +`@clerk/expo-native-components` follows good practices of security, but 100% security cannot be assured. + +`@clerk/expo-native-components` is provided **"as is"** without any **warranty**. Use at your own risk. + +_For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._ + +## License + +This project is licensed under the **MIT license**. + +See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/expo-native-components/LICENSE) for more information. diff --git a/packages/expo/android/build.gradle b/packages/expo-native-components/android/build.gradle similarity index 93% rename from packages/expo/android/build.gradle rename to packages/expo-native-components/android/build.gradle index f0d3b20858d..364224dfdf0 100644 --- a/packages/expo/android/build.gradle +++ b/packages/expo-native-components/android/build.gradle @@ -13,8 +13,9 @@ applyKotlinExpoModulesCorePlugin() group = 'com.clerk.expo' version = '1.0.0' -def clerkExpoPackageJson = new JsonSlurper().parse(new File(projectDir, "../package.json")) -def clerkExpoVersion = clerkExpoPackageJson.version.toString() +// Set by the @clerk/expo-native-components config plugin to the installed @clerk/expo version. +def clerkExpoVersion = rootProject.findProperty("clerkExpo.hostSdkVersion")?.toString() ?: + new JsonSlurper().parse(new File(projectDir, "../package.json")).version.toString() // Dependency versions - centralized for easier updates // See: https://docs.gradle.org/current/userguide/version_catalogs.html for app-level version catalogs @@ -97,7 +98,7 @@ try { } } 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, + // In that case, users should add '@clerk/expo-native-components' to their app.json plugins array instead, // which applies the flag via the config plugin's withClerkAndroid. } diff --git a/packages/expo/android/src/main/AndroidManifest.xml b/packages/expo-native-components/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/expo/android/src/main/AndroidManifest.xml rename to packages/expo-native-components/android/src/main/AndroidManifest.xml diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt similarity index 100% rename from packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt rename to packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt similarity index 100% rename from packages/expo/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt rename to packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt similarity index 100% rename from packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt rename to packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt similarity index 100% rename from packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt rename to packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt similarity index 100% rename from packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt rename to packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt similarity index 100% rename from packages/expo/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt rename to packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt diff --git a/packages/expo/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt b/packages/expo-native-components/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt similarity index 100% rename from packages/expo/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt rename to packages/expo-native-components/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt diff --git a/packages/expo/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt b/packages/expo-native-components/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt similarity index 100% rename from packages/expo/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt rename to packages/expo-native-components/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt diff --git a/packages/expo-native-components/app.plugin.d.ts b/packages/expo-native-components/app.plugin.d.ts new file mode 100644 index 00000000000..8808449dd73 --- /dev/null +++ b/packages/expo-native-components/app.plugin.d.ts @@ -0,0 +1,5 @@ +import type { ConfigPlugin } from '@expo/config-plugins'; + +declare const withClerkExpoNativeComponents: ConfigPlugin<{ keychainService?: string; theme?: string } | void>; + +export = withClerkExpoNativeComponents; diff --git a/packages/expo-native-components/app.plugin.js b/packages/expo-native-components/app.plugin.js new file mode 100644 index 00000000000..36e10092b7a --- /dev/null +++ b/packages/expo-native-components/app.plugin.js @@ -0,0 +1,333 @@ +/** + * Expo config plugin for @clerk/expo-native-components + * Configures iOS and Android for the Clerk native SDKs (clerk-ios / clerk-android) + * + * When this plugin is used: + * 1. iOS is configured with the required deployment target and metadata + * 2. Android is configured with packaging exclusions for dependencies + * + * Native modules and views are registered via Expo Modules autolinking. + */ +const { + createRunOncePlugin, + withXcodeProject, + withDangerousMod, + withInfoPlist, + withAppBuildGradle, + withGradleProperties, +} = require('@expo/config-plugins'); +const path = require('path'); +const fs = require('fs'); +const packageJson = require('./package.json'); + +const CLERK_MIN_IOS_VERSION = '17.0'; +const HOST_SDK_VERSION_GRADLE_PROPERTY = 'clerkExpo.hostSdkVersion'; + +const resolveClerkExpo = projectRoot => { + try { + const paths = [projectRoot, process.cwd()].filter(Boolean); + const clerkExpoPackageJsonPath = require.resolve('@clerk/expo/package.json', { paths }); + return { + dir: path.dirname(clerkExpoPackageJsonPath), + version: require(clerkExpoPackageJsonPath).version, + }; + } catch { + return null; + } +}; + +const withClerkIOS = (config, hostSdkVersion) => { + console.log('✅ Clerk iOS plugin loaded'); + + // IMPORTANT: Set iOS deployment target in Podfile.properties.json BEFORE pod install + // This ensures ClerkExpo pod gets installed (it requires iOS 17.0) + config = withDangerousMod(config, [ + 'ios', + async config => { + const podfilePropertiesPath = path.join(config.modRequest.platformProjectRoot, 'Podfile.properties.json'); + + let properties = {}; + if (fs.existsSync(podfilePropertiesPath)) { + try { + properties = JSON.parse(fs.readFileSync(podfilePropertiesPath, 'utf8')); + } catch { + // If file exists but is invalid JSON, start fresh + } + } + + // Set the iOS deployment target + if ( + !properties['ios.deploymentTarget'] || + parseFloat(properties['ios.deploymentTarget']) < parseFloat(CLERK_MIN_IOS_VERSION) + ) { + properties['ios.deploymentTarget'] = CLERK_MIN_IOS_VERSION; + fs.writeFileSync(podfilePropertiesPath, JSON.stringify(properties, null, 2) + '\n'); + console.log(`✅ Set ios.deploymentTarget to ${CLERK_MIN_IOS_VERSION} in Podfile.properties.json`); + } + + return config; + }, + ]); + + // First update the iOS deployment target to 17.0 (required by Clerk iOS SDK) + config = withXcodeProject(config, config => { + const xcodeProject = config.modResults; + + try { + // Update deployment target in all build configurations + const buildConfigs = xcodeProject.hash.project.objects.XCBuildConfiguration || {}; + + for (const [uuid, buildConfig] of Object.entries(buildConfigs)) { + if (buildConfig && buildConfig.buildSettings) { + const currentTarget = buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET; + if (currentTarget && parseFloat(currentTarget) < parseFloat(CLERK_MIN_IOS_VERSION)) { + buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET = CLERK_MIN_IOS_VERSION; + } + } + } + + console.log(`✅ Updated iOS deployment target to ${CLERK_MIN_IOS_VERSION}`); + } catch (error) { + console.error('❌ Error updating deployment target:', error.message); + } + + return config; + }); + + config = withInfoPlist(config, modConfig => { + modConfig.modResults.ClerkExpoVersion = hostSdkVersion; + return modConfig; + }); + + return config; +}; + +/** + * Add packaging exclusions to Android app build.gradle to resolve + * duplicate META-INF file conflicts from clerk-android dependencies. + */ +const withClerkAndroid = (config, hostSdkVersion) => { + console.log('✅ Clerk Android plugin loaded'); + + config = withGradleProperties(config, modConfig => { + modConfig.modResults = modConfig.modResults.filter( + item => !(item.type === 'property' && item.key === HOST_SDK_VERSION_GRADLE_PROPERTY), + ); + modConfig.modResults.push({ type: 'property', key: HOST_SDK_VERSION_GRADLE_PROPERTY, value: hostSdkVersion }); + return modConfig; + }); + + return withAppBuildGradle(config, modConfig => { + let buildGradle = modConfig.modResults.contents; + + // --- META-INF exclusion --- + if (!buildGradle.includes('META-INF/versions/9/OSGI-INF/MANIFEST.MF')) { + // AGP 8+ uses `packaging` DSL, older versions use `packagingOptions` + const packagingMatch = buildGradle.match(/packaging\s*\{/) || buildGradle.match(/packagingOptions\s*\{/); + if (packagingMatch) { + const blockName = packagingMatch[0].trim().replace(/\s*\{$/, ''); + const resourcesExclude = `${blockName} { + // Clerk Android SDK: exclude duplicate META-INF files + resources { + excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'] + }`; + + buildGradle = buildGradle.replace(new RegExp(`${blockName}\\s*\\{`), resourcesExclude); + } else { + // No packaging block found; append one at the end of the android block + const androidBlockEnd = buildGradle.lastIndexOf('}'); + if (androidBlockEnd !== -1) { + const packagingBlock = `\n packaging {\n resources {\n excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']\n }\n }\n`; + buildGradle = buildGradle.slice(0, androidBlockEnd) + packagingBlock + buildGradle.slice(androidBlockEnd); + } + } + 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; + return modConfig; + }); +}; + +/** + * Write ClerkKeychainService to Info.plist when keychainService is provided. + * This allows extension apps (watch, widget, app clip) to share the same + * keychain entry as the main app by using a custom service identifier. + */ +const withClerkKeychainService = (config, { keychainService } = {}) => { + if (!keychainService) { + return config; + } + + return withInfoPlist(config, modConfig => { + modConfig.modResults.ClerkKeychainService = keychainService; + console.log(`✅ Set ClerkKeychainService in Info.plist: ${keychainService}`); + return modConfig; + }); +}; + +/** + * Apply a custom theme to Clerk native components (iOS + Android). + * + * Accepts a `theme` prop pointing to a JSON file with optional keys: + * - colors: { primary, background, input, danger, success, warning, + * foreground, mutedForeground, primaryForeground, inputForeground, + * neutral, border, ring, muted, shadow, secondaryButtonBackground, + * secondaryButtonForeground } (hex color strings) + * - darkColors: same keys as colors (for dark mode) + * - design: { fontFamily: string, borderRadius: number } + * + * iOS: Embeds the parsed JSON into Info.plist under key "ClerkTheme". + * Android: Copies the JSON file to android/app/src/main/assets/clerk_theme.json. + */ +const VALID_COLOR_KEYS = [ + 'primary', + 'background', + 'input', + 'danger', + 'success', + 'warning', + 'foreground', + 'mutedForeground', + 'primaryForeground', + 'inputForeground', + 'neutral', + 'border', + 'ring', + 'muted', + 'shadow', + 'secondaryButtonBackground', + 'secondaryButtonForeground', +]; + +const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; + +function isPlainObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function validateThemeJson(theme) { + if (!isPlainObject(theme)) { + throw new Error('Clerk theme: theme JSON must be a plain object'); + } + + const validateColors = (colors, label) => { + if (!isPlainObject(colors)) { + throw new Error(`Clerk theme: ${label} must be an object`); + } + for (const [key, value] of Object.entries(colors)) { + if (!VALID_COLOR_KEYS.includes(key)) { + console.warn(`⚠️ Clerk theme: unknown color key "${key}" in ${label}, ignoring`); + continue; + } + if (typeof value !== 'string' || !HEX_COLOR_REGEX.test(value)) { + throw new Error(`Clerk theme: invalid hex color for ${label}.${key}: "${value}"`); + } + } + }; + + if (theme.colors != null) validateColors(theme.colors, 'colors'); + if (theme.darkColors != null) validateColors(theme.darkColors, 'darkColors'); + + if (theme.design != null) { + if (!isPlainObject(theme.design)) { + throw new Error(`Clerk theme: design must be an object`); + } + if (theme.design.fontFamily != null && typeof theme.design.fontFamily !== 'string') { + throw new Error(`Clerk theme: design.fontFamily must be a string`); + } + if (theme.design.borderRadius != null && typeof theme.design.borderRadius !== 'number') { + throw new Error(`Clerk theme: design.borderRadius must be a number`); + } + } +} + +const withClerkTheme = (config, props = {}) => { + const { theme } = props; + if (!theme) return config; + + // Resolve the theme file path relative to the project root + const themePath = path.resolve(theme); + if (!fs.existsSync(themePath)) { + console.warn(`⚠️ Clerk theme file not found: ${themePath}, skipping theme`); + return config; + } + + let themeJson; + try { + themeJson = JSON.parse(fs.readFileSync(themePath, 'utf8')); + validateThemeJson(themeJson); + } catch (e) { + throw new Error(`Clerk theme: failed to parse ${themePath}: ${e.message}`); + } + + // iOS: Embed theme in Info.plist under "ClerkTheme" + config = withInfoPlist(config, modConfig => { + modConfig.modResults.ClerkTheme = themeJson; + console.log('✅ Embedded Clerk theme in Info.plist'); + return modConfig; + }); + + // Android: Copy theme JSON to assets + config = withDangerousMod(config, [ + 'android', + async config => { + const assetsDir = path.join(config.modRequest.platformProjectRoot, 'app', 'src', 'main', 'assets'); + if (!fs.existsSync(assetsDir)) { + fs.mkdirSync(assetsDir, { recursive: true }); + } + const destPath = path.join(assetsDir, 'clerk_theme.json'); + fs.writeFileSync(destPath, JSON.stringify(themeJson, null, 2) + '\n'); + console.log('✅ Copied Clerk theme to Android assets'); + return config; + }, + ]); + + return config; +}; + +const withClerkExpoNativeComponents = (config, props = {}, resolve = resolveClerkExpo) => { + const clerkExpo = resolve(config._internal?.projectRoot); + if (clerkExpo && fs.existsSync(path.join(clerkExpo.dir, 'expo-module.config.json'))) { + throw new Error( + `Clerk: @clerk/expo@${clerkExpo.version} still bundles the Clerk native module, which conflicts with @clerk/expo-native-components. Upgrade @clerk/expo to a version that supports @clerk/expo-native-components.`, + ); + } + // Native requests report the @clerk/expo version in the x-clerk-host-sdk-version header. + const hostSdkVersion = clerkExpo?.version ?? packageJson.version; + + config = withClerkIOS(config, hostSdkVersion); + config = withClerkAndroid(config, hostSdkVersion); + config = withClerkKeychainService(config, props); + config = withClerkTheme(config, props); + return config; +}; + +module.exports = createRunOncePlugin(withClerkExpoNativeComponents, packageJson.name, packageJson.version); +module.exports._testing = { + withClerkExpoNativeComponents, + validateThemeJson, + isPlainObject, + VALID_COLOR_KEYS, + HEX_COLOR_REGEX, +}; diff --git a/packages/expo/expo-module.config.json b/packages/expo-native-components/expo-module.config.json similarity index 100% rename from packages/expo/expo-module.config.json rename to packages/expo-native-components/expo-module.config.json diff --git a/packages/expo/ios/ClerkAppDelegateSubscriber.swift b/packages/expo-native-components/ios/ClerkAppDelegateSubscriber.swift similarity index 100% rename from packages/expo/ios/ClerkAppDelegateSubscriber.swift rename to packages/expo-native-components/ios/ClerkAppDelegateSubscriber.swift diff --git a/packages/expo/ios/ClerkAuthNativeView.swift b/packages/expo-native-components/ios/ClerkAuthNativeView.swift similarity index 100% rename from packages/expo/ios/ClerkAuthNativeView.swift rename to packages/expo-native-components/ios/ClerkAuthNativeView.swift diff --git a/packages/expo/ios/ClerkExpo.podspec b/packages/expo-native-components/ios/ClerkExpo.podspec similarity index 100% rename from packages/expo/ios/ClerkExpo.podspec rename to packages/expo-native-components/ios/ClerkExpo.podspec diff --git a/packages/expo/ios/ClerkExpoModule.swift b/packages/expo-native-components/ios/ClerkExpoModule.swift similarity index 100% rename from packages/expo/ios/ClerkExpoModule.swift rename to packages/expo-native-components/ios/ClerkExpoModule.swift diff --git a/packages/expo/ios/ClerkNativeBridge.swift b/packages/expo-native-components/ios/ClerkNativeBridge.swift similarity index 100% rename from packages/expo/ios/ClerkNativeBridge.swift rename to packages/expo-native-components/ios/ClerkNativeBridge.swift diff --git a/packages/expo/ios/ClerkNativeViewHost.swift b/packages/expo-native-components/ios/ClerkNativeViewHost.swift similarity index 100% rename from packages/expo/ios/ClerkNativeViewHost.swift rename to packages/expo-native-components/ios/ClerkNativeViewHost.swift diff --git a/packages/expo/ios/ClerkUserButtonNativeView.swift b/packages/expo-native-components/ios/ClerkUserButtonNativeView.swift similarity index 100% rename from packages/expo/ios/ClerkUserButtonNativeView.swift rename to packages/expo-native-components/ios/ClerkUserButtonNativeView.swift diff --git a/packages/expo/ios/ClerkUserProfileNativeView.swift b/packages/expo-native-components/ios/ClerkUserProfileNativeView.swift similarity index 100% rename from packages/expo/ios/ClerkUserProfileNativeView.swift rename to packages/expo-native-components/ios/ClerkUserProfileNativeView.swift diff --git a/packages/expo/ios/Tests/ClerkAuthNativeViewPaperTests.swift b/packages/expo-native-components/ios/Tests/ClerkAuthNativeViewPaperTests.swift similarity index 100% rename from packages/expo/ios/Tests/ClerkAuthNativeViewPaperTests.swift rename to packages/expo-native-components/ios/Tests/ClerkAuthNativeViewPaperTests.swift diff --git a/packages/expo/ios/Tests/ClerkNativeBridgeTests.swift b/packages/expo-native-components/ios/Tests/ClerkNativeBridgeTests.swift similarity index 100% rename from packages/expo/ios/Tests/ClerkNativeBridgeTests.swift rename to packages/expo-native-components/ios/Tests/ClerkNativeBridgeTests.swift diff --git a/packages/expo/ios/Tests/ClerkNativeViewHostTests.swift b/packages/expo-native-components/ios/Tests/ClerkNativeViewHostTests.swift similarity index 100% rename from packages/expo/ios/Tests/ClerkNativeViewHostTests.swift rename to packages/expo-native-components/ios/Tests/ClerkNativeViewHostTests.swift diff --git a/packages/expo/ios/Tests/ClerkUserProfileCustomPageStateTests.swift b/packages/expo-native-components/ios/Tests/ClerkUserProfileCustomPageStateTests.swift similarity index 100% rename from packages/expo/ios/Tests/ClerkUserProfileCustomPageStateTests.swift rename to packages/expo-native-components/ios/Tests/ClerkUserProfileCustomPageStateTests.swift diff --git a/packages/expo-native-components/package.json b/packages/expo-native-components/package.json new file mode 100644 index 00000000000..ffae25ffc09 --- /dev/null +++ b/packages/expo-native-components/package.json @@ -0,0 +1,81 @@ +{ + "name": "@clerk/expo-native-components", + "version": "0.0.1", + "description": "Native Clerk components and native SDK integration for Expo, powered by clerk-ios and clerk-android", + "keywords": [ + "react", + "react-native", + "expo", + "clerk", + "auth", + "authentication", + "native" + ], + "homepage": "https://clerk.com/", + "bugs": { + "url": "https://github.com/clerk/javascript/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/clerk/javascript.git", + "directory": "packages/expo-native-components" + }, + "license": "MIT", + "author": "Clerk", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./app.plugin.js": "./app.plugin.js", + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "android", + "ios", + "src/specs", + "expo-module.config.json", + "react-native.config.js", + "app.plugin.js", + "app.plugin.d.ts" + ], + "scripts": { + "build": "tsdown", + "build:declarations": "tsc -p tsconfig.declarations.json", + "clean": "rimraf ./dist", + "dev": "tsdown --watch", + "dev:pub": "pnpm dev -- --env.publish", + "format": "node ../../scripts/format-package.mjs", + "format:check": "node ../../scripts/format-package.mjs --check", + "lint": "eslint src", + "test": "vitest run", + "test:watch": "vitest watch" + }, + "dependencies": { + "tslib": "catalog:repo" + }, + "devDependencies": { + "@expo/config-plugins": "^54.0.4", + "react-native": "^0.86.0" + }, + "peerDependencies": { + "expo": "catalog:peer-expo", + "react": "^18.0.0 || ^19.0.0", + "react-native": ">=0.75" + }, + "engines": { + "node": ">=20.9.0" + }, + "publishConfig": { + "access": "public" + }, + "codegenConfig": { + "name": "ClerkExpoSpec", + "type": "all", + "jsSrcsDir": "src/specs" + } +} diff --git a/packages/expo/react-native.config.js b/packages/expo-native-components/react-native.config.js similarity index 100% rename from packages/expo/react-native.config.js rename to packages/expo-native-components/react-native.config.js diff --git a/packages/expo/src/native/AuthView.tsx b/packages/expo-native-components/src/AuthView.tsx similarity index 86% rename from packages/expo/src/native/AuthView.tsx rename to packages/expo-native-components/src/AuthView.tsx index 3f4b51ee346..30e4e374d57 100644 --- a/packages/expo/src/native/AuthView.tsx +++ b/packages/expo-native-components/src/AuthView.tsx @@ -2,9 +2,9 @@ import { useCallback } from 'react'; import type { NativeSyntheticEvent } from 'react-native'; import { Text, View } from 'react-native'; -import NativeClerkAuthView from '../specs/NativeClerkAuthView'; -import { isNativeSupported } from '../utils/native-module'; import type { AuthViewProps } from './AuthView.types'; +import NativeClerkAuthView from './specs/NativeClerkAuthView'; +import { isNativeSupported } from './utils/native-module'; type AuthNativeEvent = NativeSyntheticEvent>; @@ -24,7 +24,7 @@ type AuthNativeEvent = NativeSyntheticEvent>; * * @example * ```tsx - * import { AuthView } from '@clerk/expo/native'; + * import { AuthView } from '@clerk/expo/native-components'; * import { useAuth } from '@clerk/expo'; * * export default function SignInScreen() { @@ -63,7 +63,7 @@ export function AuthView({ {!isNativeSupported ? 'Native AuthView is only available on iOS and Android' - : 'Native AuthView requires the @clerk/expo plugin. Add "@clerk/expo" to your app.json plugins array.'} + : 'Native AuthView requires a development build with the @clerk/expo-native-components config plugin. Add "@clerk/expo-native-components" to your app.json plugins array and rebuild your native app.'} ); diff --git a/packages/expo/src/native/AuthView.types.ts b/packages/expo-native-components/src/AuthView.types.ts similarity index 100% rename from packages/expo/src/native/AuthView.types.ts rename to packages/expo-native-components/src/AuthView.types.ts diff --git a/packages/expo/src/native/EmbeddedNavigation.types.ts b/packages/expo-native-components/src/EmbeddedNavigation.types.ts similarity index 100% rename from packages/expo/src/native/EmbeddedNavigation.types.ts rename to packages/expo-native-components/src/EmbeddedNavigation.types.ts diff --git a/packages/expo/src/native/UserButton.tsx b/packages/expo-native-components/src/UserButton.tsx similarity index 94% rename from packages/expo/src/native/UserButton.tsx rename to packages/expo-native-components/src/UserButton.tsx index a1406f0652e..30bff0e825d 100644 --- a/packages/expo/src/native/UserButton.tsx +++ b/packages/expo-native-components/src/UserButton.tsx @@ -3,8 +3,7 @@ import { useMemo, useRef } from 'react'; import type { NativeSyntheticEvent } from 'react-native'; import { StyleSheet, useWindowDimensions } from 'react-native'; -import NativeClerkUserButtonView from '../specs/NativeClerkUserButtonView'; -import { isNativeSupported } from '../utils/native-module'; +import NativeClerkUserButtonView from './specs/NativeClerkUserButtonView'; import type { NativeUserProfileNavigationHandle, UserProfileCustomDestination, @@ -16,6 +15,7 @@ import { UserProfileCustomPageHosts, useUserProfileCustomPages, } from './UserProfileCustomPages'; +import { isNativeSupported } from './utils/native-module'; type CustomizableNativeUserButtonProps = ComponentProps> & { customPages?: string; @@ -47,7 +47,7 @@ export interface UserButtonProps { * * @example * ```tsx - * import { UserButton } from '@clerk/expo/native'; + * import { UserButton } from '@clerk/expo/native-components'; * * export default function Home() { * return ( diff --git a/packages/expo/src/native/UserProfileCustomPages.tsx b/packages/expo-native-components/src/UserProfileCustomPages.tsx similarity index 100% rename from packages/expo/src/native/UserProfileCustomPages.tsx rename to packages/expo-native-components/src/UserProfileCustomPages.tsx diff --git a/packages/expo/src/native/UserProfileView.tsx b/packages/expo-native-components/src/UserProfileView.tsx similarity index 92% rename from packages/expo/src/native/UserProfileView.tsx rename to packages/expo-native-components/src/UserProfileView.tsx index 3c39d955375..ee48098f1e8 100644 --- a/packages/expo/src/native/UserProfileView.tsx +++ b/packages/expo-native-components/src/UserProfileView.tsx @@ -3,9 +3,8 @@ import { useCallback, useMemo, useRef } from 'react'; import type { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native'; import { StyleSheet, Text, View } from 'react-native'; -import NativeClerkUserProfileView from '../specs/NativeClerkUserProfileView'; -import { isNativeSupported } from '../utils/native-module'; import type { EmbeddedNavigationProps } from './EmbeddedNavigation.types'; +import NativeClerkUserProfileView from './specs/NativeClerkUserProfileView'; import type { NativeUserProfileNavigationHandle, UserProfileCustomDestination, @@ -17,6 +16,7 @@ import { UserProfileCustomPageHosts, useUserProfileCustomPages, } from './UserProfileCustomPages'; +import { isNativeSupported } from './utils/native-module'; type CustomizableNativeUserProfileProps = ComponentProps> & { customPages?: string; @@ -75,7 +75,7 @@ export interface UserProfileViewProps extends EmbeddedNavigationProps { * * @example * ```tsx - * import { UserProfileView } from '@clerk/expo/native'; + * import { UserProfileView } from '@clerk/expo/native-components'; * import { useAuth } from '@clerk/expo'; * * export default function ProfileScreen() { @@ -128,7 +128,7 @@ export function UserProfileView({ {!isNativeSupported ? 'Native UserProfileView is only available on iOS and Android' - : 'Native UserProfileView requires the @clerk/expo plugin. Add "@clerk/expo" to your app.json plugins array.'} + : 'Native UserProfileView requires a development build with the @clerk/expo-native-components config plugin. Add "@clerk/expo-native-components" to your app.json plugins array and rebuild your native app.'} ); diff --git a/packages/expo/src/native/__tests__/AuthView.test.tsx b/packages/expo-native-components/src/__tests__/AuthView.test.tsx similarity index 94% rename from packages/expo/src/native/__tests__/AuthView.test.tsx rename to packages/expo-native-components/src/__tests__/AuthView.test.tsx index 6b3029ecdda..528e251fb2e 100644 --- a/packages/expo/src/native/__tests__/AuthView.test.tsx +++ b/packages/expo-native-components/src/__tests__/AuthView.test.tsx @@ -10,13 +10,13 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock('../../specs/NativeClerkAuthView', () => { +vi.mock('../specs/NativeClerkAuthView', () => { return { default: mocks.NativeClerkAuthView, }; }); -vi.mock('../../utils/native-module', () => { +vi.mock('../utils/native-module', () => { return { isNativeSupported: true, }; diff --git a/packages/expo/src/native/__tests__/UserButton.test.tsx b/packages/expo-native-components/src/__tests__/UserButton.test.tsx similarity index 94% rename from packages/expo/src/native/__tests__/UserButton.test.tsx rename to packages/expo-native-components/src/__tests__/UserButton.test.tsx index d4eaef80382..d414aa0f89f 100644 --- a/packages/expo/src/native/__tests__/UserButton.test.tsx +++ b/packages/expo-native-components/src/__tests__/UserButton.test.tsx @@ -8,14 +8,14 @@ const mocks = vi.hoisted(() => ({ nativeProps: vi.fn(), })); -vi.mock('../../specs/NativeClerkUserButtonView', () => ({ +vi.mock('../specs/NativeClerkUserButtonView', () => ({ default: React.forwardRef((props: Record, _ref) => { mocks.nativeProps(props); return null; }), })); -vi.mock('../../utils/native-module', () => ({ +vi.mock('../utils/native-module', () => ({ isNativeSupported: true, })); diff --git a/packages/expo/src/native/__tests__/UserProfileCustomPages.test.tsx b/packages/expo-native-components/src/__tests__/UserProfileCustomPages.test.tsx similarity index 100% rename from packages/expo/src/native/__tests__/UserProfileCustomPages.test.tsx rename to packages/expo-native-components/src/__tests__/UserProfileCustomPages.test.tsx diff --git a/packages/expo/src/native/__tests__/UserProfileView.test.tsx b/packages/expo-native-components/src/__tests__/UserProfileView.test.tsx similarity index 98% rename from packages/expo/src/native/__tests__/UserProfileView.test.tsx rename to packages/expo-native-components/src/__tests__/UserProfileView.test.tsx index 34af7275e6d..619c417305e 100644 --- a/packages/expo/src/native/__tests__/UserProfileView.test.tsx +++ b/packages/expo-native-components/src/__tests__/UserProfileView.test.tsx @@ -14,7 +14,7 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock('../../specs/NativeClerkUserProfileView', () => { +vi.mock('../specs/NativeClerkUserProfileView', () => { return { default: React.forwardRef((props: { children?: React.ReactNode }, ref) => { React.useImperativeHandle(ref, () => ({ navigateCustomPage: mocks.navigateCustomPage })); @@ -24,7 +24,7 @@ vi.mock('../../specs/NativeClerkUserProfileView', () => { }; }); -vi.mock('../../utils/native-module', () => { +vi.mock('../utils/native-module', () => { return { isNativeSupported: true, }; diff --git a/packages/expo-native-components/src/__tests__/appPlugin.test.js b/packages/expo-native-components/src/__tests__/appPlugin.test.js new file mode 100644 index 00000000000..bbb2d247250 --- /dev/null +++ b/packages/expo-native-components/src/__tests__/appPlugin.test.js @@ -0,0 +1,53 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports -- CJS plugin, no ESM export +const { withClerkExpoNativeComponents } = require('../../app.plugin.js')._testing; + +const applyMod = (config, platform, mod, modResults) => + config.mods[platform][mod]({ ...config, modRequest: {}, modResults }); + +describe('withClerkExpoNativeComponents', () => { + let clerkExpoDir; + + beforeEach(() => { + vi.spyOn(console, 'log').mockImplementation(() => {}); + clerkExpoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'clerk-expo-')); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(clerkExpoDir, { recursive: true, force: true }); + }); + + test('reports the installed @clerk/expo version on iOS and Android', async () => { + const config = withClerkExpoNativeComponents({ name: 'test', slug: 'test' }, {}, () => ({ + dir: clerkExpoDir, + version: '4.8.0', + })); + + const infoPlist = await applyMod(config, 'ios', 'infoPlist', {}); + const gradleProperties = await applyMod(config, 'android', 'gradleProperties', [ + { type: 'property', key: 'clerkExpo.hostSdkVersion', value: '4.7.0' }, + ]); + + expect(infoPlist.modResults.ClerkExpoVersion).toBe('4.8.0'); + expect(gradleProperties.modResults).toEqual([ + { type: 'property', key: 'clerkExpo.hostSdkVersion', value: '4.8.0' }, + ]); + }); + + test('throws when the installed @clerk/expo still bundles the native module', () => { + fs.writeFileSync(path.join(clerkExpoDir, 'expo-module.config.json'), '{}'); + + expect(() => + withClerkExpoNativeComponents({ name: 'test', slug: 'test' }, {}, () => ({ + dir: clerkExpoDir, + version: '4.7.1', + })), + ).toThrow('@clerk/expo@4.7.1 still bundles the Clerk native module'); + }); +}); diff --git a/packages/expo/src/__tests__/appPlugin.theme.test.js b/packages/expo-native-components/src/__tests__/appPlugin.theme.test.js similarity index 71% rename from packages/expo/src/__tests__/appPlugin.theme.test.js rename to packages/expo-native-components/src/__tests__/appPlugin.theme.test.js index fcff7577b0e..c0f89d02ab8 100644 --- a/packages/expo/src/__tests__/appPlugin.theme.test.js +++ b/packages/expo-native-components/src/__tests__/appPlugin.theme.test.js @@ -2,54 +2,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; // eslint-disable-next-line @typescript-eslint/no-require-imports -- CJS plugin, no ESM export const clerkPlugin = require('../../app.plugin.js'); -const { withClerkFaceIDPermission, validateThemeJson } = clerkPlugin._testing; - -function applyInfoPlistMod(config, modResults) { - return config.mods.ios.infoPlist({ - ...config, - modRequest: {}, - modResults, - }); -} - -describe('withClerkFaceIDPermission', () => { - test('adds the configured Face ID usage description', async () => { - const config = withClerkFaceIDPermission( - { name: 'test', slug: 'test' }, - { faceIDPermission: 'Allow $(PRODUCT_NAME) to use Face ID for secure sign-in.' }, - ); - - const result = await applyInfoPlistMod(config, {}); - - expect(result.modResults.NSFaceIDUsageDescription).toBe('Allow $(PRODUCT_NAME) to use Face ID for secure sign-in.'); - }); - - test('preserves an app-provided Face ID usage description', async () => { - const config = withClerkFaceIDPermission( - { name: 'test', slug: 'test' }, - { faceIDPermission: 'Clerk-provided description' }, - ); - - const result = await applyInfoPlistMod(config, { - NSFaceIDUsageDescription: 'App-provided description', - }); - - expect(result.modResults.NSFaceIDUsageDescription).toBe('App-provided description'); - }); - - test('does not configure the Info.plist without an explicit permission description', () => { - const config = { name: 'test', slug: 'test' }; - - expect(withClerkFaceIDPermission(config)).toBe(config); - expect(config).not.toHaveProperty('mods'); - }); - - test.each([null, '', ' ', true])('rejects an invalid permission description: %j', faceIDPermission => { - expect(() => withClerkFaceIDPermission({ name: 'test', slug: 'test' }, { faceIDPermission })).toThrow( - 'faceIDPermission must be a non-empty string', - ); - }); -}); +const { validateThemeJson } = clerkPlugin._testing; describe('validateThemeJson', () => { beforeEach(() => { diff --git a/packages/expo-native-components/src/index.ts b/packages/expo-native-components/src/index.ts new file mode 100644 index 00000000000..b687c560a74 --- /dev/null +++ b/packages/expo-native-components/src/index.ts @@ -0,0 +1,47 @@ +/** + * Native UI components for Clerk authentication in Expo apps. + * + * These components provide pre-built, native authentication experiences powered by: + * - **iOS**: clerk-ios (SwiftUI) - https://github.com/clerk/clerk-ios + * - **Android**: clerk-android (Jetpack Compose) - https://github.com/clerk/clerk-android + * + * ## Installation + * + * Native components require the `@clerk/expo-native-components` config plugin in your `app.json`, alongside `@clerk/expo`: + * + * ```json + * { + * "expo": { + * "plugins": ["@clerk/expo", "@clerk/expo-native-components"] + * } + * } + * ``` + * + * Then run `npx expo prebuild` to generate native code. + * + * ## Components + * + * - {@link AuthView} - Authentication flow (sign-in/sign-up), renders inline + * - {@link UserProfileView} - User profile and account management, renders inline + * - {@link UserButton} - Avatar button that opens the native user profile + * + * @module @clerk/expo-native-components + */ + +export { AuthView } from './AuthView'; +export type { AuthViewProps, AuthViewMode } from './AuthView.types'; +export type { EmbeddedNavigationProps } from './EmbeddedNavigation.types'; +export { UserButton } from './UserButton'; +export type { UserButtonProps, UserButtonUserProfileProps } from './UserButton'; +export { useUserProfileCustomPageNavigation } from './UserProfileCustomPages'; +export type { + UserProfileCustomPageNavigation, + UserProfileCustomDestination, + UserProfileCustomPage, + UserProfileCustomPageIcon, + UserProfileCustomPagePlacement, + UserProfileRow, + UserProfileSection, +} from './UserProfileCustomPages'; +export { UserProfileView } from './UserProfileView'; +export type { UserProfileViewProps } from './UserProfileView'; diff --git a/packages/expo/src/specs/NativeClerkAuthView.android.ts b/packages/expo-native-components/src/specs/NativeClerkAuthView.android.ts similarity index 100% rename from packages/expo/src/specs/NativeClerkAuthView.android.ts rename to packages/expo-native-components/src/specs/NativeClerkAuthView.android.ts diff --git a/packages/expo/src/specs/NativeClerkAuthView.ts b/packages/expo-native-components/src/specs/NativeClerkAuthView.ts similarity index 100% rename from packages/expo/src/specs/NativeClerkAuthView.ts rename to packages/expo-native-components/src/specs/NativeClerkAuthView.ts diff --git a/packages/expo/src/specs/NativeClerkUserButtonView.android.ts b/packages/expo-native-components/src/specs/NativeClerkUserButtonView.android.ts similarity index 100% rename from packages/expo/src/specs/NativeClerkUserButtonView.android.ts rename to packages/expo-native-components/src/specs/NativeClerkUserButtonView.android.ts diff --git a/packages/expo/src/specs/NativeClerkUserButtonView.ts b/packages/expo-native-components/src/specs/NativeClerkUserButtonView.ts similarity index 100% rename from packages/expo/src/specs/NativeClerkUserButtonView.ts rename to packages/expo-native-components/src/specs/NativeClerkUserButtonView.ts diff --git a/packages/expo/src/specs/NativeClerkUserProfileView.android.ts b/packages/expo-native-components/src/specs/NativeClerkUserProfileView.android.ts similarity index 100% rename from packages/expo/src/specs/NativeClerkUserProfileView.android.ts rename to packages/expo-native-components/src/specs/NativeClerkUserProfileView.android.ts diff --git a/packages/expo/src/specs/NativeClerkUserProfileView.ts b/packages/expo-native-components/src/specs/NativeClerkUserProfileView.ts similarity index 100% rename from packages/expo/src/specs/NativeClerkUserProfileView.ts rename to packages/expo-native-components/src/specs/NativeClerkUserProfileView.ts diff --git a/packages/expo/src/specs/__tests__/native-view.web.test.ts b/packages/expo-native-components/src/specs/__tests__/native-view.web.test.ts similarity index 100% rename from packages/expo/src/specs/__tests__/native-view.web.test.ts rename to packages/expo-native-components/src/specs/__tests__/native-view.web.test.ts diff --git a/packages/expo-native-components/src/utils/native-module.ts b/packages/expo-native-components/src/utils/native-module.ts new file mode 100644 index 00000000000..a0044c25834 --- /dev/null +++ b/packages/expo-native-components/src/utils/native-module.ts @@ -0,0 +1,3 @@ +import { Platform } from 'react-native'; + +export const isNativeSupported = Platform.OS === 'ios' || Platform.OS === 'android'; diff --git a/packages/expo-native-components/tsconfig.declarations.json b/packages/expo-native-components/tsconfig.declarations.json new file mode 100644 index 00000000000..ac04a85ce27 --- /dev/null +++ b/packages/expo-native-components/tsconfig.declarations.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "incremental": false, + "skipLibCheck": true, + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": true, + "sourceMap": false, + "declarationDir": "./dist" + }, + "include": ["src"], + "exclude": ["**/__tests__/**/*", "app.plugin.js"] +} diff --git a/packages/expo-native-components/tsconfig.json b/packages/expo-native-components/tsconfig.json new file mode 100644 index 00000000000..5510c6beb95 --- /dev/null +++ b/packages/expo-native-components/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "outDir": "dist", + "lib": ["es2019", "dom"], + "jsx": "react-jsx", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "importHelpers": true, + "declaration": true, + "declarationMap": false, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "sourceMap": false, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowJs": true, + "target": "ES2019", + "noEmitOnError": false, + "incremental": true, + "moduleSuffixes": [".web", ".ios", ".android", ".native", ""] + }, + "include": ["src", "app.plugin.js"] +} diff --git a/packages/expo-native-components/tsconfig.test.json b/packages/expo-native-components/tsconfig.test.json new file mode 100644 index 00000000000..5635d6cd1b7 --- /dev/null +++ b/packages/expo-native-components/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": true + } +} diff --git a/packages/expo-native-components/tsdown.config.mts b/packages/expo-native-components/tsdown.config.mts new file mode 100644 index 00000000000..e84f7249174 --- /dev/null +++ b/packages/expo-native-components/tsdown.config.mts @@ -0,0 +1,33 @@ +import type { Options } from 'tsdown'; +import { defineConfig } from 'tsdown'; + +import { runAfterLast } from '../../scripts/utils.ts'; + +function preserveRelativeImports(id: string) { + return id.startsWith('.'); +} + +export default defineConfig(overrideOptions => { + const isWatch = !!overrideOptions.watch; + const shouldPublish = !!overrideOptions.env?.publish; + + const options: Options = { + format: 'cjs', + fixedExtension: false, + outDir: './dist', + entry: ['./src/**/*.{ts,tsx,js,jsx}', '!./src/**/*.test.{ts,tsx,js}', '!./src/**/__tests__/**'], + unbundle: true, + clean: true, + minify: false, + sourcemap: true, + deps: { + // Keep relative import specifiers unchanged so Metro can apply platform-specific resolution. + neverBundle: preserveRelativeImports, + }, + define: { + __DEV__: `${isWatch}`, + }, + }; + + return runAfterLast(['pnpm build:declarations', shouldPublish && 'pkglab pub --ping'])(options); +}); diff --git a/packages/expo-native-components/vitest.config.mts b/packages/expo-native-components/vitest.config.mts new file mode 100644 index 00000000000..20bc22a33db --- /dev/null +++ b/packages/expo-native-components/vitest.config.mts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [], + test: { + environment: 'jsdom', + includeSource: ['**/*.{js,ts,jsx,tsx}'], + setupFiles: './vitest.setup.mts', + }, +}); diff --git a/packages/expo-native-components/vitest.setup.mts b/packages/expo-native-components/vitest.setup.mts new file mode 100644 index 00000000000..283c324f550 --- /dev/null +++ b/packages/expo-native-components/vitest.setup.mts @@ -0,0 +1,22 @@ +import { cleanup } from '@testing-library/react'; +import { afterEach, beforeAll, vi } from 'vitest'; + +// Mock globalThis.expo for expo-modules-core +if (!globalThis.expo) { + // @ts-expect-error - Mocking expo for tests + globalThis.expo = { + EventEmitter: vi.fn(), + }; +} + +// Define __DEV__ for expo-modules-core +if (typeof globalThis.__DEV__ === 'undefined') { + // @ts-expect-error - Mocking __DEV__ for tests + globalThis.__DEV__ = false; +} + +beforeAll(() => {}); + +afterEach(() => { + cleanup(); +}); diff --git a/packages/expo/app.plugin.js b/packages/expo/app.plugin.js index f8483877ff3..ecba866b513 100644 --- a/packages/expo/app.plugin.js +++ b/packages/expo/app.plugin.js @@ -1,27 +1,15 @@ /** - * Expo config plugin for @clerk/clerk-expo - * Automatically configures iOS and Android to work with Clerk native components + * Expo config plugin for @clerk/expo * * When this plugin is used: - * 1. iOS is configured with the required deployment target and metadata - * 2. Android is configured with packaging exclusions for dependencies - * - * Native modules and views are registered via Expo Modules autolinking. + * 1. Android registers the hosted auth callback intent filter + * 2. iOS gets the Sign in with Apple entitlement and, when configured, the Face ID usage description + * 3. If @clerk/expo-native-components is installed, its config plugin is applied for the Clerk native SDKs */ -const { - AndroidConfig, - withXcodeProject, - withDangerousMod, - withInfoPlist, - withAppBuildGradle, - withAndroidManifest, - withEntitlementsPlist, -} = require('@expo/config-plugins'); -const path = require('path'); -const fs = require('fs'); -const packageJson = require('./package.json'); +const { AndroidConfig, withAndroidManifest, withEntitlementsPlist, withInfoPlist } = require('@expo/config-plugins'); -const CLERK_MIN_IOS_VERSION = '17.0'; +const CLERK_EXPO_NATIVE = '@clerk/expo-native-components'; +const CLERK_EXPO_NATIVE_OPTIONS = ['keychainService', 'theme']; const addHostedAuthIntentFilter = (mainActivity, packageName) => { const callbackHost = `${packageName}.hosted-callback`; @@ -52,80 +40,8 @@ const addHostedAuthIntentFilter = (mainActivity, packageName) => { mainActivity['intent-filter'] = intentFilters; }; -const withClerkIOS = config => { - console.log('✅ Clerk iOS plugin loaded'); - - // IMPORTANT: Set iOS deployment target in Podfile.properties.json BEFORE pod install - // This ensures ClerkExpo pod gets installed (it requires iOS 17.0) - config = withDangerousMod(config, [ - 'ios', - async config => { - const podfilePropertiesPath = path.join(config.modRequest.platformProjectRoot, 'Podfile.properties.json'); - - let properties = {}; - if (fs.existsSync(podfilePropertiesPath)) { - try { - properties = JSON.parse(fs.readFileSync(podfilePropertiesPath, 'utf8')); - } catch { - // If file exists but is invalid JSON, start fresh - } - } - - // Set the iOS deployment target - if ( - !properties['ios.deploymentTarget'] || - parseFloat(properties['ios.deploymentTarget']) < parseFloat(CLERK_MIN_IOS_VERSION) - ) { - properties['ios.deploymentTarget'] = CLERK_MIN_IOS_VERSION; - fs.writeFileSync(podfilePropertiesPath, JSON.stringify(properties, null, 2) + '\n'); - console.log(`✅ Set ios.deploymentTarget to ${CLERK_MIN_IOS_VERSION} in Podfile.properties.json`); - } - - return config; - }, - ]); - - // First update the iOS deployment target to 17.0 (required by Clerk iOS SDK) - config = withXcodeProject(config, config => { - const xcodeProject = config.modResults; - - try { - // Update deployment target in all build configurations - const buildConfigs = xcodeProject.hash.project.objects.XCBuildConfiguration || {}; - - for (const [uuid, buildConfig] of Object.entries(buildConfigs)) { - if (buildConfig && buildConfig.buildSettings) { - const currentTarget = buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET; - if (currentTarget && parseFloat(currentTarget) < parseFloat(CLERK_MIN_IOS_VERSION)) { - buildConfig.buildSettings.IPHONEOS_DEPLOYMENT_TARGET = CLERK_MIN_IOS_VERSION; - } - } - } - - console.log(`✅ Updated iOS deployment target to ${CLERK_MIN_IOS_VERSION}`); - } catch (error) { - console.error('❌ Error updating deployment target:', error.message); - } - - return config; - }); - - config = withInfoPlist(config, modConfig => { - modConfig.modResults.ClerkExpoVersion = packageJson.version; - return modConfig; - }); - - return config; -}; - -/** - * Add packaging exclusions to Android app build.gradle to resolve - * duplicate META-INF file conflicts from clerk-android dependencies. - */ -const withClerkAndroid = config => { - console.log('✅ Clerk Android plugin loaded'); - - config = withAndroidManifest(config, modConfig => { +const withClerkHostedAuthCallback = config => { + return withAndroidManifest(config, modConfig => { const packageName = config.android?.package; if (packageName) { const mainActivity = AndroidConfig.Manifest.getMainActivityOrThrow(modConfig.modResults); @@ -133,83 +49,6 @@ const withClerkAndroid = config => { } return modConfig; }); - - return withAppBuildGradle(config, modConfig => { - let buildGradle = modConfig.modResults.contents; - - // --- META-INF exclusion --- - if (!buildGradle.includes('META-INF/versions/9/OSGI-INF/MANIFEST.MF')) { - // AGP 8+ uses `packaging` DSL, older versions use `packagingOptions` - const packagingMatch = buildGradle.match(/packaging\s*\{/) || buildGradle.match(/packagingOptions\s*\{/); - if (packagingMatch) { - const blockName = packagingMatch[0].trim().replace(/\s*\{$/, ''); - const resourcesExclude = `${blockName} { - // Clerk Android SDK: exclude duplicate META-INF files - resources { - excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'] - }`; - - buildGradle = buildGradle.replace(new RegExp(`${blockName}\\s*\\{`), resourcesExclude); - } else { - // No packaging block found; append one at the end of the android block - const androidBlockEnd = buildGradle.lastIndexOf('}'); - if (androidBlockEnd !== -1) { - const packagingBlock = `\n packaging {\n resources {\n excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']\n }\n }\n`; - buildGradle = buildGradle.slice(0, androidBlockEnd) + packagingBlock + buildGradle.slice(androidBlockEnd); - } - } - 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; - return modConfig; - }); -}; - -/** - * Combined Clerk Expo plugin - * - * When this plugin is configured in app.json/app.config.js: - * 1. iOS gets the deployment target and metadata required by Clerk native views - * 2. Android gets packaging exclusions for dependency conflicts - * - * Native modules and views are registered via Expo Modules autolinking. - */ -/** - * Write ClerkKeychainService to Info.plist when keychainService is provided. - * This allows extension apps (watch, widget, app clip) to share the same - * keychain entry as the main app by using a custom service identifier. - */ -const withClerkKeychainService = (config, { keychainService } = {}) => { - if (!keychainService) { - return config; - } - - return withInfoPlist(config, modConfig => { - modConfig.modResults.ClerkKeychainService = keychainService; - console.log(`✅ Set ClerkKeychainService in Info.plist: ${keychainService}`); - return modConfig; - }); }; const withClerkFaceIDPermission = (config, { faceIDPermission } = {}) => { @@ -243,145 +82,75 @@ const withClerkAppleSignIn = config => { }); }; -/** - * Apply a custom theme to Clerk native components (iOS + Android). - * - * Accepts a `theme` prop pointing to a JSON file with optional keys: - * - colors: { primary, background, input, danger, success, warning, - * foreground, mutedForeground, primaryForeground, inputForeground, - * neutral, border, ring, muted, shadow, secondaryButtonBackground, - * secondaryButtonForeground } (hex color strings) - * - darkColors: same keys as colors (for dark mode) - * - design: { fontFamily: string, borderRadius: number } - * - * iOS: Embeds the parsed JSON into Info.plist under key "ClerkTheme". - * Android: Copies the JSON file to android/app/src/main/assets/clerk_theme.json. - */ -const VALID_COLOR_KEYS = [ - 'primary', - 'background', - 'input', - 'danger', - 'success', - 'warning', - 'foreground', - 'mutedForeground', - 'primaryForeground', - 'inputForeground', - 'neutral', - 'border', - 'ring', - 'muted', - 'shadow', - 'secondaryButtonBackground', - 'secondaryButtonForeground', -]; - -const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; - -function isPlainObject(value) { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function validateThemeJson(theme) { - if (!isPlainObject(theme)) { - throw new Error('Clerk theme: theme JSON must be a plain object'); +const resolveClerkExpoNativePlugin = config => { + try { + const paths = [config._internal?.projectRoot, process.cwd()].filter(Boolean); + return require(require.resolve(`${CLERK_EXPO_NATIVE}/app.plugin.js`, { paths })); + } catch { + return null; } +}; - const validateColors = (colors, label) => { - if (!isPlainObject(colors)) { - throw new Error(`Clerk theme: ${label} must be an object`); - } - for (const [key, value] of Object.entries(colors)) { - if (!VALID_COLOR_KEYS.includes(key)) { - console.warn(`⚠️ Clerk theme: unknown color key "${key}" in ${label}, ignoring`); - continue; - } - if (typeof value !== 'string' || !HEX_COLOR_REGEX.test(value)) { - throw new Error(`Clerk theme: invalid hex color for ${label}.${key}: "${value}"`); - } - } - }; - - if (theme.colors != null) validateColors(theme.colors, 'colors'); - if (theme.darkColors != null) validateColors(theme.darkColors, 'darkColors'); - - if (theme.design != null) { - if (!isPlainObject(theme.design)) { - throw new Error(`Clerk theme: design must be an object`); +const getListedPluginProps = (config, name) => { + for (const entry of config.plugins || []) { + if (entry === name) { + return {}; } - if (theme.design.fontFamily != null && typeof theme.design.fontFamily !== 'string') { - throw new Error(`Clerk theme: design.fontFamily must be a string`); - } - if (theme.design.borderRadius != null && typeof theme.design.borderRadius !== 'number') { - throw new Error(`Clerk theme: design.borderRadius must be a number`); + if (Array.isArray(entry) && entry[0] === name) { + return entry[1] || {}; } } -} - -const withClerkTheme = (config, props = {}) => { - const { theme } = props; - if (!theme) return config; + return {}; +}; - // Resolve the theme file path relative to the project root - const themePath = path.resolve(theme); - if (!fs.existsSync(themePath)) { - console.warn(`⚠️ Clerk theme file not found: ${themePath}, skipping theme`); +/** + * Apply the @clerk/expo-native-components config plugin when it is installed, so apps that only list + * "@clerk/expo" keep the iOS deployment target and native SDK configuration they need. + */ +const withClerkExpoNativeComponents = (config, props = {}, resolvePlugin = resolveClerkExpoNativePlugin) => { + const nativeProps = Object.fromEntries( + CLERK_EXPO_NATIVE_OPTIONS.filter(option => props[option] !== undefined).map(option => [option, props[option]]), + ); + const nativeOptionNames = Object.keys(nativeProps) + .map(option => `"${option}"`) + .join(', '); + + if (config._internal?.pluginHistory?.[CLERK_EXPO_NATIVE]) { + if (nativeOptionNames) { + console.warn( + `⚠️ Clerk: The following "@clerk/expo" plugin options are ignored because the "${CLERK_EXPO_NATIVE}" plugin already ran: ${nativeOptionNames}. Pass them to the "${CLERK_EXPO_NATIVE}" plugin instead.`, + ); + } return config; } - let themeJson; - try { - themeJson = JSON.parse(fs.readFileSync(themePath, 'utf8')); - validateThemeJson(themeJson); - } catch (e) { - throw new Error(`Clerk theme: failed to parse ${themePath}: ${e.message}`); + const clerkExpoNativePlugin = resolvePlugin(config); + if (!clerkExpoNativePlugin) { + if (nativeOptionNames) { + console.warn( + `⚠️ Clerk: The following "@clerk/expo" plugin options require ${CLERK_EXPO_NATIVE} and are ignored: ${nativeOptionNames}. Install it with \`npx expo install ${CLERK_EXPO_NATIVE}\` and add "${CLERK_EXPO_NATIVE}" to the plugins array in your app config.`, + ); + } + return config; } - // iOS: Embed theme in Info.plist under "ClerkTheme" - config = withInfoPlist(config, modConfig => { - modConfig.modResults.ClerkTheme = themeJson; - console.log('✅ Embedded Clerk theme in Info.plist'); - return modConfig; - }); - - // Android: Copy theme JSON to assets - config = withDangerousMod(config, [ - 'android', - async config => { - const assetsDir = path.join(config.modRequest.platformProjectRoot, 'app', 'src', 'main', 'assets'); - if (!fs.existsSync(assetsDir)) { - fs.mkdirSync(assetsDir, { recursive: true }); - } - const destPath = path.join(assetsDir, 'clerk_theme.json'); - fs.writeFileSync(destPath, JSON.stringify(themeJson, null, 2) + '\n'); - console.log('✅ Copied Clerk theme to Android assets'); - return config; - }, - ]); - - return config; + return clerkExpoNativePlugin(config, { ...nativeProps, ...getListedPluginProps(config, CLERK_EXPO_NATIVE) }); }; const withClerkExpo = (config, props = {}) => { const { appleSignIn = true } = props; - config = withClerkIOS(config); if (appleSignIn !== false) { config = withClerkAppleSignIn(config); } - config = withClerkAndroid(config); - config = withClerkKeychainService(config, props); + config = withClerkHostedAuthCallback(config); config = withClerkFaceIDPermission(config, props); - config = withClerkTheme(config, props); + config = withClerkExpoNativeComponents(config, props); return config; }; module.exports = withClerkExpo; module.exports._testing = { addHostedAuthIntentFilter, + withClerkExpoNativeComponents, withClerkFaceIDPermission, - validateThemeJson, - isPlainObject, - VALID_COLOR_KEYS, - HEX_COLOR_REGEX, }; diff --git a/packages/expo/native-components/index.d.ts b/packages/expo/native-components/index.d.ts new file mode 100644 index 00000000000..567539c519f --- /dev/null +++ b/packages/expo/native-components/index.d.ts @@ -0,0 +1,3 @@ +export * from '@clerk/expo-native-components'; +export { useAuthViewState } from '../dist/native-components/useAuthViewState'; +export type { UseAuthViewStateReturn } from '../dist/native-components/useAuthViewState'; diff --git a/packages/expo/native-components/package.json b/packages/expo/native-components/package.json new file mode 100644 index 00000000000..0ac9927d9ba --- /dev/null +++ b/packages/expo/native-components/package.json @@ -0,0 +1,4 @@ +{ + "main": "../dist/native-components/index.js", + "types": "./index.d.ts" +} diff --git a/packages/expo/native/index.d.ts b/packages/expo/native/index.d.ts new file mode 100644 index 00000000000..0df12c4c5cd --- /dev/null +++ b/packages/expo/native/index.d.ts @@ -0,0 +1 @@ +export * from '../native-components/index'; diff --git a/packages/expo/native/package.json b/packages/expo/native/package.json index 6ae24b71af4..928da2cc7c8 100644 --- a/packages/expo/native/package.json +++ b/packages/expo/native/package.json @@ -1,4 +1,4 @@ { "main": "../dist/native/index.js", - "types": "../dist/native/index.d.ts" + "types": "./index.d.ts" } diff --git a/packages/expo/package.json b/packages/expo/package.json index d3c8b0249d1..eacbcc996ff 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -29,8 +29,12 @@ "default": "./dist/index.js" }, "./app.plugin.js": "./app.plugin.js", + "./native-components": { + "types": "./native-components/index.d.ts", + "default": "./dist/native-components/index.js" + }, "./native": { - "types": "./dist/native/index.d.ts", + "types": "./native/index.d.ts", "default": "./dist/native/index.js" }, "./web": { @@ -89,8 +93,7 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "android", - "ios", + "native-components", "native", "web", "local-credentials", @@ -104,9 +107,6 @@ "hosted-auth", "experimental", "legacy", - "src/specs", - "expo-module.config.json", - "react-native.config.js", "app.plugin.js", "app.plugin.d.ts" ], @@ -133,6 +133,7 @@ "devDependencies": { "@clerk/expo-biometrics": "workspace:*", "@clerk/expo-google-signin": "workspace:*", + "@clerk/expo-native-components": "workspace:*", "@clerk/expo-passkeys": "workspace:*", "@expo/config-plugins": "^54.0.4", "@types/base-64": "^1.0.2", @@ -149,6 +150,7 @@ "peerDependencies": { "@clerk/expo-biometrics": ">=0.1.0", "@clerk/expo-google-signin": ">=0.1.0", + "@clerk/expo-native-components": ">=0.1.0", "@clerk/expo-passkeys": ">=0.0.6", "expo": "catalog:peer-expo", "expo-apple-authentication": ">=7.0.0", @@ -169,6 +171,9 @@ "@clerk/expo-google-signin": { "optional": true }, + "@clerk/expo-native-components": { + "optional": true + }, "@clerk/expo-passkeys": { "optional": true }, @@ -202,10 +207,5 @@ }, "publishConfig": { "access": "public" - }, - "codegenConfig": { - "name": "ClerkExpoSpec", - "type": "all", - "jsSrcsDir": "src/specs" } } diff --git a/packages/expo/src/__tests__/appPlugin.test.js b/packages/expo/src/__tests__/appPlugin.test.js new file mode 100644 index 00000000000..55f9e5abdc3 --- /dev/null +++ b/packages/expo/src/__tests__/appPlugin.test.js @@ -0,0 +1,128 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports -- CJS plugin, no ESM export +const { withClerkExpoNativeComponents, withClerkFaceIDPermission } = require('../../app.plugin.js')._testing; + +function applyInfoPlistMod(config, modResults) { + return config.mods.ios.infoPlist({ + ...config, + modRequest: {}, + modResults, + }); +} + +describe('withClerkFaceIDPermission', () => { + test('adds the configured Face ID usage description', async () => { + const config = withClerkFaceIDPermission( + { name: 'test', slug: 'test' }, + { faceIDPermission: 'Allow $(PRODUCT_NAME) to use Face ID for secure sign-in.' }, + ); + + const result = await applyInfoPlistMod(config, {}); + + expect(result.modResults.NSFaceIDUsageDescription).toBe('Allow $(PRODUCT_NAME) to use Face ID for secure sign-in.'); + }); + + test('preserves an app-provided Face ID usage description', async () => { + const config = withClerkFaceIDPermission( + { name: 'test', slug: 'test' }, + { faceIDPermission: 'Clerk-provided description' }, + ); + + const result = await applyInfoPlistMod(config, { + NSFaceIDUsageDescription: 'App-provided description', + }); + + expect(result.modResults.NSFaceIDUsageDescription).toBe('App-provided description'); + }); + + test('does not configure the Info.plist without an explicit permission description', () => { + const config = { name: 'test', slug: 'test' }; + + expect(withClerkFaceIDPermission(config)).toBe(config); + expect(config).not.toHaveProperty('mods'); + }); + + test.each([null, '', ' ', true])('rejects an invalid permission description: %j', faceIDPermission => { + expect(() => withClerkFaceIDPermission({ name: 'test', slug: 'test' }, { faceIDPermission })).toThrow( + 'faceIDPermission must be a non-empty string', + ); + }); +}); + +describe('withClerkExpoNativeComponents', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('applies the @clerk/expo-native-components plugin with its options when it is installed', () => { + const nativePlugin = vi.fn(config => ({ ...config, applied: true })); + const config = { name: 'test', slug: 'test' }; + + const result = withClerkExpoNativeComponents( + config, + { keychainService: 'com.example.shared', theme: './theme.json', appleSignIn: false }, + () => nativePlugin, + ); + + expect(result.applied).toBe(true); + expect(nativePlugin).toHaveBeenCalledWith(config, { keychainService: 'com.example.shared', theme: './theme.json' }); + }); + + test('prefers options passed to an explicitly listed @clerk/expo-native-components plugin', () => { + const nativePlugin = vi.fn(config => config); + const config = { + name: 'test', + slug: 'test', + plugins: ['@clerk/expo', ['@clerk/expo-native-components', { theme: './native-theme.json' }]], + }; + + withClerkExpoNativeComponents( + config, + { keychainService: 'com.example.shared', theme: './theme.json' }, + () => nativePlugin, + ); + + expect(nativePlugin).toHaveBeenCalledWith(config, { + keychainService: 'com.example.shared', + theme: './native-theme.json', + }); + }); + + test('does not apply the @clerk/expo-native-components plugin twice', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const nativePlugin = vi.fn(config => config); + const config = { + name: 'test', + slug: 'test', + _internal: { + pluginHistory: { '@clerk/expo-native-components': { name: '@clerk/expo-native-components', version: '0.0.1' } }, + }, + }; + + expect(withClerkExpoNativeComponents(config, {}, () => nativePlugin)).toBe(config); + expect(nativePlugin).not.toHaveBeenCalled(); + expect(warn).not.toHaveBeenCalled(); + + withClerkExpoNativeComponents(config, { theme: './theme.json' }, () => nativePlugin); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('Pass them to the "@clerk/expo-native-components" plugin instead'), + ); + }); + + test('leaves the config untouched when @clerk/expo-native-components is not installed', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const config = { name: 'test', slug: 'test' }; + + expect(withClerkExpoNativeComponents(config, {}, () => null)).toBe(config); + expect(warn).not.toHaveBeenCalled(); + }); + + test('warns with install instructions when native-only options are passed without @clerk/expo-native-components', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const config = { name: 'test', slug: 'test' }; + + expect(withClerkExpoNativeComponents(config, { keychainService: 'com.example.shared' }, () => null)).toBe(config); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('npx expo install @clerk/expo-native-components')); + }); +}); diff --git a/packages/expo/src/native-components/__tests__/index.test.ts b/packages/expo/src/native-components/__tests__/index.test.ts new file mode 100644 index 00000000000..44608e1116f --- /dev/null +++ b/packages/expo/src/native-components/__tests__/index.test.ts @@ -0,0 +1,61 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('../useAuthViewState', () => ({ useAuthViewState: vi.fn() })); + +const importNativeEntry = () => import('../index'); + +describe('@clerk/expo/native-components', () => { + afterEach(() => { + vi.doUnmock('../loadClerkExpoNative'); + vi.resetModules(); + vi.restoreAllMocks(); + }); + + describe('when @clerk/expo-native-components is not installed', () => { + beforeEach(() => { + vi.doMock('../loadClerkExpoNative', () => ({ loadClerkExpoNative: () => null })); + }); + + test.each(['AuthView', 'UserButton', 'UserProfileView'] as const)( + 'rendering %s throws an error with install instructions', + async name => { + const Component = (await importNativeEntry())[name] as (props: object) => unknown; + + expect(() => Component({})).toThrow( + /`.+` is unavailable\. Native components have moved to the @clerk\/expo-native-components package\. Install it with `npx expo install @clerk\/expo-native-components`/, + ); + }, + ); + + test('calling useUserProfileCustomPageNavigation throws an error with install instructions', async () => { + const hook = (await importNativeEntry()).useUserProfileCustomPageNavigation as () => unknown; + + expect(() => hook()).toThrow('`useUserProfileCustomPageNavigation` is unavailable.'); + expect(() => hook()).toThrow('add "@clerk/expo-native-components" to the plugins array in your app config'); + }); + + test('exports its own useAuthViewState', async () => { + const nativeEntry = await importNativeEntry(); + const { useAuthViewState } = await import('../useAuthViewState'); + + expect(nativeEntry.useAuthViewState).toBe(useAuthViewState); + }); + }); + + test('re-exports @clerk/expo-native-components when it is installed', async () => { + const clerkExpoNative = { + AuthView: vi.fn(() => null), + UserButton: vi.fn(() => null), + UserProfileView: vi.fn(() => null), + useUserProfileCustomPageNavigation: vi.fn(), + }; + vi.doMock('../loadClerkExpoNative', () => ({ loadClerkExpoNative: () => clerkExpoNative })); + + const nativeEntry = await importNativeEntry(); + + expect(nativeEntry.AuthView).toBe(clerkExpoNative.AuthView); + expect(nativeEntry.UserButton).toBe(clerkExpoNative.UserButton); + expect(nativeEntry.UserProfileView).toBe(clerkExpoNative.UserProfileView); + expect(nativeEntry.useUserProfileCustomPageNavigation).toBe(clerkExpoNative.useUserProfileCustomPageNavigation); + }); +}); diff --git a/packages/expo/src/native/__tests__/useAuthViewState.test.tsx b/packages/expo/src/native-components/__tests__/useAuthViewState.test.tsx similarity index 100% rename from packages/expo/src/native/__tests__/useAuthViewState.test.tsx rename to packages/expo/src/native-components/__tests__/useAuthViewState.test.tsx diff --git a/packages/expo/src/native-components/index.ts b/packages/expo/src/native-components/index.ts new file mode 100644 index 00000000000..9054ae159ed --- /dev/null +++ b/packages/expo/src/native-components/index.ts @@ -0,0 +1,27 @@ +import { errorThrower } from '../errorThrower'; +import type { ClerkExpoNativeModule } from './loadClerkExpoNative'; +import { loadClerkExpoNative } from './loadClerkExpoNative'; + +// Public types are declared in native-components/index.d.ts. + +const CLERK_EXPO_NATIVE_MISSING_MESSAGE = + 'Native components have moved to the @clerk/expo-native-components package. ' + + 'Install it with `npx expo install @clerk/expo-native-components`, add "@clerk/expo-native-components" to the plugins array in your app config, ' + + 'then rebuild your native app.'; + +const clerkExpoNative = loadClerkExpoNative(); + +function resolveExport(name: K) { + return ( + clerkExpoNative?.[name] ?? + function ClerkExpoNativeMissing(): never { + return errorThrower.throw(`\`${name}\` is unavailable. ${CLERK_EXPO_NATIVE_MISSING_MESSAGE}`); + } + ); +} + +export const AuthView = resolveExport('AuthView'); +export const UserButton = resolveExport('UserButton'); +export const UserProfileView = resolveExport('UserProfileView'); +export { useAuthViewState } from './useAuthViewState'; +export const useUserProfileCustomPageNavigation = resolveExport('useUserProfileCustomPageNavigation'); diff --git a/packages/expo/src/native-components/loadClerkExpoNative.ts b/packages/expo/src/native-components/loadClerkExpoNative.ts new file mode 100644 index 00000000000..d9822cc40ea --- /dev/null +++ b/packages/expo/src/native-components/loadClerkExpoNative.ts @@ -0,0 +1,15 @@ +type ClerkExpoNativeExport = (...args: never[]) => unknown; + +export type ClerkExpoNativeModule = Partial< + Record<'AuthView' | 'UserButton' | 'UserProfileView' | 'useUserProfileCustomPageNavigation', ClerkExpoNativeExport> +>; + +export function loadClerkExpoNative(): ClerkExpoNativeModule | null { + try { + // Synchronous require() in try/catch so Metro treats @clerk/expo-native-components as an optional dependency. + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require('@clerk/expo-native-components') as ClerkExpoNativeModule; + } catch { + return null; + } +} diff --git a/packages/expo/src/native/useAuthViewState.ts b/packages/expo/src/native-components/useAuthViewState.ts similarity index 100% rename from packages/expo/src/native/useAuthViewState.ts rename to packages/expo/src/native-components/useAuthViewState.ts diff --git a/packages/expo/src/native/__tests__/index.test.ts b/packages/expo/src/native/__tests__/index.test.ts new file mode 100644 index 00000000000..415e020935b --- /dev/null +++ b/packages/expo/src/native/__tests__/index.test.ts @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +const nativeComponentsEntry = vi.hoisted(() => ({ + AuthView: () => null, + UserButton: () => null, + UserProfileView: () => null, + useAuthViewState: () => null, + useUserProfileCustomPageNavigation: () => null, +})); +const deprecated = vi.hoisted(() => vi.fn()); + +vi.mock('../../native-components', () => nativeComponentsEntry); +vi.mock('@clerk/shared/deprecated', () => ({ deprecated })); + +describe('@clerk/expo/native', () => { + afterEach(() => { + vi.resetModules(); + deprecated.mockClear(); + }); + + test('re-exports everything from @clerk/expo/native-components', async () => { + const nativeEntry = await import('../index'); + + expect({ ...nativeEntry }).toEqual(nativeComponentsEntry); + }); + + test('warns that the entry point is deprecated when it is imported', async () => { + await import('../index'); + + expect(deprecated).toHaveBeenCalledExactlyOnceWith( + '@clerk/expo/native', + 'Import from `@clerk/expo/native-components` instead.', + ); + }); +}); diff --git a/packages/expo/src/native/index.ts b/packages/expo/src/native/index.ts index 2a62047dd85..d6521aa212c 100644 --- a/packages/expo/src/native/index.ts +++ b/packages/expo/src/native/index.ts @@ -1,49 +1,5 @@ -/** - * Native UI components for Clerk authentication in Expo apps. - * - * These components provide pre-built, native authentication experiences powered by: - * - **iOS**: clerk-ios (SwiftUI) - https://github.com/clerk/clerk-ios - * - **Android**: clerk-android (Jetpack Compose) - https://github.com/clerk/clerk-android - * - * ## Installation - * - * Native components require the `@clerk/expo` plugin to be configured in your `app.json`: - * - * ```json - * { - * "expo": { - * "plugins": ["@clerk/expo"] - * } - * } - * ``` - * - * Then run `npx expo prebuild` to generate native code. - * - * ## Components - * - * - {@link AuthView} - Authentication flow (sign-in/sign-up), renders inline - * - {@link UserProfileView} - User profile and account management, renders inline - * - {@link UserButton} - Avatar button that opens the native user profile - * - * @module @clerk/expo/native - */ +import { deprecated } from '@clerk/shared/deprecated'; -export { AuthView } from './AuthView'; -export type { AuthViewProps, AuthViewMode } from './AuthView.types'; -export type { EmbeddedNavigationProps } from './EmbeddedNavigation.types'; -export { useAuthViewState } from './useAuthViewState'; -export type { UseAuthViewStateReturn } from './useAuthViewState'; -export { UserButton } from './UserButton'; -export type { UserButtonProps, UserButtonUserProfileProps } from './UserButton'; -export { useUserProfileCustomPageNavigation } from './UserProfileCustomPages'; -export type { - UserProfileCustomPageNavigation, - UserProfileCustomDestination, - UserProfileCustomPage, - UserProfileCustomPageIcon, - UserProfileCustomPagePlacement, - UserProfileRow, - UserProfileSection, -} from './UserProfileCustomPages'; -export { UserProfileView } from './UserProfileView'; -export type { UserProfileViewProps } from './UserProfileView'; +deprecated('@clerk/expo/native', 'Import from `@clerk/expo/native-components` instead.'); + +export * from '../native-components'; diff --git a/packages/expo/src/provider/nativeClientSync.tsx b/packages/expo/src/provider/nativeClientSync.tsx index 5f40ed5287c..5cdb87dc6ae 100644 --- a/packages/expo/src/provider/nativeClientSync.tsx +++ b/packages/expo/src/provider/nativeClientSync.tsx @@ -1163,7 +1163,7 @@ export function useNativeClientBootstrap({ if (__DEV__) { console.debug( `[ClerkProvider] Native Clerk module not available. ` + - `To enable native features, add "@clerk/expo" to your app.json plugins array.`, + `To enable native features, install @clerk/expo-native-components and add "@clerk/expo-native-components" to your app.json plugins array.`, ); } } else if (__DEV__) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 474f60c5838..b9b9d2fa3f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -671,6 +671,9 @@ importers: '@clerk/expo-google-signin': specifier: workspace:* version: link:../expo-google-signin + '@clerk/expo-native-components': + specifier: workspace:* + version: link:../expo-native-components '@clerk/expo-passkeys': specifier: workspace:* version: link:../expo-passkeys @@ -723,6 +726,25 @@ importers: specifier: ~54.0.36 version: 54.0.36(@babel/core@7.29.7)(bufferutil@4.1.0)(graphql@16.14.1)(react-native@0.86.0(@babel/core@7.29.7)(@react-native-community/cli@12.3.7(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@types/react@18.3.28)(bufferutil@4.1.0)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10) + packages/expo-native-components: + dependencies: + expo: + specifier: catalog:peer-expo + version: 54.0.36(@babel/core@7.29.7)(bufferutil@4.1.0)(graphql@16.14.1)(react-native@0.86.0(@babel/core@7.29.7)(@react-native-community/cli@12.3.7(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@types/react@18.3.28)(bufferutil@4.1.0)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: + specifier: 18.3.1 + version: 18.3.1 + tslib: + specifier: catalog:repo + version: 2.8.1 + devDependencies: + '@expo/config-plugins': + specifier: ^54.0.4 + version: 54.0.5 + react-native: + specifier: ^0.86.0 + version: 0.86.0(@babel/core@7.29.7)(@react-native-community/cli@12.3.7(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@types/react@18.3.28)(bufferutil@4.1.0)(react@18.3.1)(utf-8-validate@5.0.10) + packages/expo-passkeys: dependencies: '@clerk/shared':