diff --git a/.changeset/align-experimental-unstable-prefixes.md b/.changeset/align-experimental-unstable-prefixes.md new file mode 100644 index 00000000000..2fb499c615c --- /dev/null +++ b/.changeset/align-experimental-unstable-prefixes.md @@ -0,0 +1,45 @@ +--- +"@clerk/clerk-js": major +"@clerk/react": major +"@clerk/nextjs": major +"@clerk/vue": major +"@clerk/astro": major +"@clerk/expo": major +"@clerk/chrome-extension": major +"@clerk/shared": major +"@clerk/ui": major +--- + +Align experimental/unstable prefixes to use consistent naming: + +- Renamed all `__unstable_*` methods to `__internal_*` (for internal APIs) +- Renamed all `experimental__*` and `experimental_*` methods to `__experimental_*` (for beta features) +- Removed deprecated billing-related props and `experimental__forceOauthFirst` +- Moved `createTheme` and `simple` to `@clerk/ui/themes/experimental` export path (removed `__experimental_` prefix since they're now in the experimental export) + +**Breaking Changes:** + +### @clerk/clerk-js +- `__unstable__environment` → `__internal_environment` +- `__unstable__updateProps` → `__internal_updateProps` +- `__unstable__setEnvironment` → `__internal_setEnvironment` +- `__unstable__onBeforeRequest` → `__internal_onBeforeRequest` +- `__unstable__onAfterResponse` → `__internal_onAfterResponse` +- `__unstable__onBeforeSetActive` → `__internal_onBeforeSetActive` (window global) +- `__unstable__onAfterSetActive` → `__internal_onAfterSetActive` (window global) + +### @clerk/nextjs +- `__unstable_invokeMiddlewareOnAuthStateChange` → `__internal_invokeMiddlewareOnAuthStateChange` + +### @clerk/ui +- `experimental_createTheme` / `__experimental_createTheme` → `createTheme` (now exported from `@clerk/ui/themes/experimental`) +- `experimental__simple` / `__experimental_simple` → `simple` (now exported from `@clerk/ui/themes/experimental`) + +### @clerk/chrome-extension +- `__unstable__createClerkClient` → `createClerkClient` (exported from `@clerk/chrome-extension/background`) + +### Removed (multiple packages) +- `__unstable_manageBillingUrl` (removed) +- `__unstable_manageBillingLabel` (removed) +- `__unstable_manageBillingMembersLimit` (removed) +- `experimental__forceOauthFirst` (removed) diff --git a/.changeset/bright-zebras-arrive.md b/.changeset/bright-zebras-arrive.md new file mode 100644 index 00000000000..980cce4ef43 --- /dev/null +++ b/.changeset/bright-zebras-arrive.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': major +--- + +Updates both `colorRing` and `colorModalBackdrop` to render at full opacity when modified via the appearance prop or CSS variables. Previously we'd render the provided color at 15% opacity, which made it difficult to dial in a specific ring or backdrop color. diff --git a/.changeset/cuddly-shrimps-refuse.md b/.changeset/cuddly-shrimps-refuse.md new file mode 100644 index 00000000000..8b706ab25f5 --- /dev/null +++ b/.changeset/cuddly-shrimps-refuse.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': major +--- + +Update engines config to require node@20 or higher diff --git a/.changeset/cyan-dancers-chew.md b/.changeset/cyan-dancers-chew.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/cyan-dancers-chew.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/eight-groups-poke.md b/.changeset/eight-groups-poke.md new file mode 100644 index 00000000000..ff0832737c6 --- /dev/null +++ b/.changeset/eight-groups-poke.md @@ -0,0 +1,12 @@ +--- +'@clerk/upgrade': minor +--- + +Add support for the latest versions of the following packages: +- `@clerk/react` (replacement for `@clerk/react`) +- `@clerk/expo` (replacement for `@clerk/expo`) +- `@clerk/nextjs` +- `@clerk/react-router` +- `@clerk/tanstack-start-react` + +During the upgrade, imports of the `useSignIn()` and `useSignUp()` hooks will be updated to import from the `/legacy` subpath. diff --git a/.changeset/fine-symbols-occur.md b/.changeset/fine-symbols-occur.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/fine-symbols-occur.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/fix-ui-cicd.md b/.changeset/fix-ui-cicd.md new file mode 100644 index 00000000000..4bfec3e10d8 --- /dev/null +++ b/.changeset/fix-ui-cicd.md @@ -0,0 +1,6 @@ +--- +"@clerk/ui": patch +--- + +Fix UI package serving in CI/CD integration tests + diff --git a/.changeset/fruity-apes-deny.md b/.changeset/fruity-apes-deny.md new file mode 100644 index 00000000000..3f4e1aaf0a6 --- /dev/null +++ b/.changeset/fruity-apes-deny.md @@ -0,0 +1,39 @@ +--- +'@clerk/clerk-js': major +'@clerk/shared': major +'@clerk/react': major +--- + +Updated returned values of `Clerk.checkout()` and `useCheckout`. + +### Vanilla JS +```ts +// Before +const { getState, subscribe, confirm, start, clear, finalize } = Clerk.checkout({ planId: "xxx", planPeriod: "annual" }) +getState().isStarting +getState().isConfirming +getState().error +getState().checkout +getState().fetchStatus +getState().status + +// After +const { checkout, errors, fetchStatus } = Clerk.checkout({ planId: "xxx", planPeriod: "annual" }) +checkout.plan // null or defined based on `checkout.status` +checkout.status +checkout.start +checkout.confirm +``` + +### React +```ts +// Before +const { id, plan, status, start, confirm, paymentSource } = useCheckout({ planId: "xxx", planPeriod: "annual" }) + +// After +const { checkout, errors, fetchStatus } = usecCheckout({ planId: "xxx", planPeriod: "annual" }) +checkout.plan // null or defined based on `checkout.status` +checkout.status +checkout.start +checkout.confirm +``` \ No newline at end of file diff --git a/.changeset/fuzzy-chefs-stand.md b/.changeset/fuzzy-chefs-stand.md new file mode 100644 index 00000000000..875ce1de5a5 --- /dev/null +++ b/.changeset/fuzzy-chefs-stand.md @@ -0,0 +1,7 @@ +--- +'@clerk/nextjs': major +'@clerk/shared': major +'@clerk/react': major +--- + +Updating minimum version of Node to v20.9.0 diff --git a/.changeset/happy-apes-care.md b/.changeset/happy-apes-care.md new file mode 100644 index 00000000000..05764126cb3 --- /dev/null +++ b/.changeset/happy-apes-care.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': major +'@clerk/shared': major +--- + +Remove deprecated `saml` property from `UserSettings` in favor of `enterpriseSSO` diff --git a/.changeset/happy-breads-begin.md b/.changeset/happy-breads-begin.md new file mode 100644 index 00000000000..622cff39209 --- /dev/null +++ b/.changeset/happy-breads-begin.md @@ -0,0 +1,8 @@ +--- +'@clerk/clerk-js': major +'@clerk/backend': major +'@clerk/shared': major +'@clerk/ui': major +--- + +Remove deprecated `samlAccount` in favor of `enterpriseAccount` diff --git a/.changeset/hide-optional-fields-by-default.md b/.changeset/hide-optional-fields-by-default.md new file mode 100644 index 00000000000..7d3f00342bc --- /dev/null +++ b/.changeset/hide-optional-fields-by-default.md @@ -0,0 +1,6 @@ +--- +'@clerk/ui': minor +--- + +Changed the default value of `appearance.layout.showOptionalFields` from `true` to `false`. Optional fields are now hidden by default during sign up. Users can still explicitly set `showOptionalFields: true` to show optional fields. + diff --git a/.changeset/hungry-beers-slide.md b/.changeset/hungry-beers-slide.md new file mode 100644 index 00000000000..1a965dad658 --- /dev/null +++ b/.changeset/hungry-beers-slide.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': major +--- + +Hide "Create organization" action when user reaches organization membership limit diff --git a/.changeset/mean-owls-brake.md b/.changeset/mean-owls-brake.md new file mode 100644 index 00000000000..7c70303998d --- /dev/null +++ b/.changeset/mean-owls-brake.md @@ -0,0 +1,17 @@ +--- +"@clerk/astro": major +"@clerk/chrome-extension": major +"@clerk/clerk-js": major +"@clerk/dev-cli": major +"@clerk/expo": major +"@clerk/express": major +"@clerk/localizations": major +"@clerk/nuxt": major +"@clerk/tanstack-react-start": major +"@clerk/testing": major +"@clerk/upgrade": major +"@clerk/vue": major +--- + +Require Node.js 20.9.0 in all packages + diff --git a/.changeset/moody-peaches-stare.md b/.changeset/moody-peaches-stare.md new file mode 100644 index 00000000000..ef4b6c9691f --- /dev/null +++ b/.changeset/moody-peaches-stare.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': major +--- + +Throw an error when an encryption key is missing when passing a secret key at runtime `clerkMiddleware()`. To migrate, ensure your application specifies a `CLERK_ENCRYPTION_KEY` environment variable when passing `secretKey` as a runtime option. diff --git a/.changeset/ninety-days-dream.md b/.changeset/ninety-days-dream.md new file mode 100644 index 00000000000..f2ae53ff06f --- /dev/null +++ b/.changeset/ninety-days-dream.md @@ -0,0 +1,8 @@ +--- +'@clerk/shared': major +'@clerk/ui': major +--- + +Remove deprecated `hideSlug` in favor of `organizationSettings.slug.disabled` setting + +Slugs can now be enabled directly from the Organization Settings page in the Clerk Dashboard diff --git a/.changeset/odd-rice-swim.md b/.changeset/odd-rice-swim.md new file mode 100644 index 00000000000..1dd941b386e --- /dev/null +++ b/.changeset/odd-rice-swim.md @@ -0,0 +1,9 @@ +--- +'@clerk/ui': major +--- + +Removes `simple` theme export from UI package in favor of using the `simple` theme via the appearance prop: + +```tsx + +``` diff --git a/.changeset/orange-hotels-join.md b/.changeset/orange-hotels-join.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/orange-hotels-join.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/remove-deprecated-props.md b/.changeset/remove-deprecated-props.md new file mode 100644 index 00000000000..913464ff3cc --- /dev/null +++ b/.changeset/remove-deprecated-props.md @@ -0,0 +1,11 @@ +--- +"@clerk/nextjs": major +"@clerk/react": major +"@clerk/clerk-js": major +"@clerk/shared": major +"@clerk/ui": major +"@clerk/react-router": major +"@clerk/tanstack-react-start": minor +--- + +Remove all previously deprecated UI props across the Next.js, React and clerk-js SDKs. The legacy `afterSign(In|Up)Url`/`redirectUrl` props, `UserButton` sign-out overrides, organization `hideSlug` flags, `OrganizationSwitcher`'s `afterSwitchOrganizationUrl`, `Client.activeSessions`, `setActive({ beforeEmit })`, and the `ClerkMiddlewareAuthObject` type alias are no longer exported. Components now rely solely on the new redirect options and server-side configuration. diff --git a/.changeset/rename-appearance-layout-to-options.md b/.changeset/rename-appearance-layout-to-options.md new file mode 100644 index 00000000000..54f3c8343f1 --- /dev/null +++ b/.changeset/rename-appearance-layout-to-options.md @@ -0,0 +1,6 @@ +--- +'@clerk/ui': major +--- + +Renamed `appearance.layout` to `appearance.options` across all appearance configurations. This is a breaking change - update all instances of `appearance.layout` to `appearance.options` in your codebase. + diff --git a/.changeset/salty-maps-fry.md b/.changeset/salty-maps-fry.md new file mode 100644 index 00000000000..17fcbef3683 --- /dev/null +++ b/.changeset/salty-maps-fry.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': major +'@clerk/shared': major +'@clerk/ui': major +--- + +Remove deprecated `saml` strategy in favor of `enterprise_sso` diff --git a/.changeset/set-minimum-expo-53.md b/.changeset/set-minimum-expo-53.md new file mode 100644 index 00000000000..9b1bdf48f3d --- /dev/null +++ b/.changeset/set-minimum-expo-53.md @@ -0,0 +1,37 @@ +--- +'@clerk/expo': major +'@clerk/expo-passkeys': major +'@clerk/shared': major +'@clerk/react': major +'@clerk/localizations': major +--- + +Drop support for Expo 50, 51 and 52. This release includes two breaking changes: + +## 1. Updated Expo peer dependency requirements + +**@clerk/expo** +- **Added** new peer dependency: `expo: >=53 <55` + - The core `expo` package is now explicitly required as a peer dependency + - This ensures compatibility with the Expo SDK version range that supports the features used by Clerk + +**@clerk/expo-passkeys** +- **Updated** peer dependency: `expo: >=53 <55` (previously `>=50 <55`) + - Minimum Expo version increased from 50 to 53 + - This aligns with the main `@clerk/expo` package requirements + +## 2. Removed legacy subpath exports + +The following packages have removed their legacy subpath export mappings: +- `@clerk/expo` +- `@clerk/shared` +- `@clerk/react` +- `@clerk/localizations` + +**What changed:** +Previously, these packages used a workaround to support subpath imports (e.g., `@clerk/shared/react`, `@clerk/expo/web`). These legacy exports have been removed in favor of modern package.json `exports` field configuration. + +All public APIs remain available through the main package entry points. + + + diff --git a/.changeset/shaky-books-occur.md b/.changeset/shaky-books-occur.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/shaky-books-occur.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/slimy-sheep-kick.md b/.changeset/slimy-sheep-kick.md new file mode 100644 index 00000000000..48f651eef5a --- /dev/null +++ b/.changeset/slimy-sheep-kick.md @@ -0,0 +1,15 @@ +--- +"@clerk/nuxt": major +--- + +Removed deprecated `getAuth()` helper. Use `event.context.auth()` in your server routes instead. + +```ts +export default defineEventHandler((event) => { + const { userId } = event.context.auth() + + return { + userId, + } +}) +``` diff --git a/.changeset/strict-hornets-kneel.md b/.changeset/strict-hornets-kneel.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/strict-hornets-kneel.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/strong-bars-learn.md b/.changeset/strong-bars-learn.md new file mode 100644 index 00000000000..1f6d62ffcc3 --- /dev/null +++ b/.changeset/strong-bars-learn.md @@ -0,0 +1,9 @@ +--- +'@clerk/chrome-extension': patch +'@clerk/expo': patch +'@clerk/nextjs': patch +'@clerk/react-router': patch +'@clerk/tanstack-react-start': patch +--- + +Use new `@clerk/react` package. diff --git a/.changeset/tall-snails-dance.md b/.changeset/tall-snails-dance.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/tall-snails-dance.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/tame-suits-try.md b/.changeset/tame-suits-try.md new file mode 100644 index 00000000000..4f8e78ae499 --- /dev/null +++ b/.changeset/tame-suits-try.md @@ -0,0 +1,5 @@ +--- +'@clerk/react': major +--- + +Change package name to `@clerk/react`. diff --git a/.changeset/ten-wolves-attack.md b/.changeset/ten-wolves-attack.md new file mode 100644 index 00000000000..fe551c47575 --- /dev/null +++ b/.changeset/ten-wolves-attack.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': major +--- + +Drop support for `next@13` and `next@14` since they have reached [EOL](https://nextjs.org/support-policy#unsupported-versions). Now `>= next@15.2.3` is required. diff --git a/.changeset/thirty-cherries-pull.md b/.changeset/thirty-cherries-pull.md new file mode 100644 index 00000000000..bb6317e0c6f --- /dev/null +++ b/.changeset/thirty-cherries-pull.md @@ -0,0 +1,12 @@ +--- +"@clerk/nuxt": major +--- + +Routing strategy for the ff. components now default to `path`: + +- `` +- `` +- `` +- `` +- `` +- `` diff --git a/.changeset/tricky-humans-stand.md b/.changeset/tricky-humans-stand.md new file mode 100644 index 00000000000..b91ce44192e --- /dev/null +++ b/.changeset/tricky-humans-stand.md @@ -0,0 +1,5 @@ +--- +'@clerk/expo': major +--- + +Rename package to `@clerk/expo`. diff --git a/.changeset/twenty-rockets-stop.md b/.changeset/twenty-rockets-stop.md new file mode 100644 index 00000000000..59744b67074 --- /dev/null +++ b/.changeset/twenty-rockets-stop.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': major +--- + +Removing deprecated top-level exports from @clerk/shared diff --git a/.changeset/wild-bees-explode.md b/.changeset/wild-bees-explode.md new file mode 100644 index 00000000000..151b584c52b --- /dev/null +++ b/.changeset/wild-bees-explode.md @@ -0,0 +1,5 @@ +--- +"@clerk/vue": minor +--- + +Introduced internal composable for handling routing configuration for UI components diff --git a/.cursor/rules/monorepo.mdc b/.cursor/rules/monorepo.mdc index e7b45bc0abd..5d00749620d 100644 --- a/.cursor/rules/monorepo.mdc +++ b/.cursor/rules/monorepo.mdc @@ -22,7 +22,6 @@ Core Package Categories - **Backend**: `@clerk/backend` - Server-side utilities and JWT verification - **Shared Utilities**: `@clerk/shared`, `@clerk/types` - Common utilities and TypeScript types - **Developer Tools**: `@clerk/testing`, `@clerk/dev-cli`, `@clerk/upgrade` -- **UI Libraries**: `@clerk/elements` - Unstyled UI primitives, `@clerk/themes` - Pre-built themes - **Specialized**: `@clerk/agent-toolkit` - AI agent integration tools Directory Structure @@ -46,11 +45,10 @@ Development Workflow Framework-Specific Packages - `@clerk/nextjs` - Next.js App Router and Pages Router support -- `@clerk/clerk-react` - React hooks and components +- `@clerk/react` - React hooks and components - `@clerk/vue` - Vue.js composables and components - `@clerk/astro` - Astro integration with SSR support - `@clerk/nuxt` - Nuxt.js module -- `@clerk/remix` - Remix loader and action utilities - `@clerk/express` - Express.js middleware - `@clerk/fastify` - Fastify plugin - `@clerk/expo` - React Native/Expo SDK @@ -105,3 +103,11 @@ Release Management - Canary and snapshot releases for testing - Git tags and GitHub releases for version tracking - Coordinated releases to maintain compatibility between packages + +Commit Messages + +- Commit messages must follow the conventional commit format using lowercase +- Commit messages must be in English +- Commit messages must be concise and to the point +- Commit messages must be prefixed with the type of change +- Commit messages must be prefixed with the scope of the change (package name or `js` for `clekr-js`, repo, release, e2e, \*) diff --git a/.github/actions/verdaccio/action.yml b/.github/actions/verdaccio/action.yml index 5279ac08666..6764b719d49 100644 --- a/.github/actions/verdaccio/action.yml +++ b/.github/actions/verdaccio/action.yml @@ -71,3 +71,37 @@ runs: pnpm config set $(echo ${{ inputs.registry }} | sed -E 's/https?://')/:_authToken secretToken # Verify proxy is working by trying to fetch a known package pnpm view semver > /dev/null 2>&1 || echo "Warning: Could not fetch semver package, proxy might not be working" + + - name: Print published Clerk package versions + shell: bash + run: | + echo "Published @clerk packages (snapshot versions):" + echo "==================================================" + packages=( + "@clerk/agent-toolkit" + "@clerk/astro" + "@clerk/backend" + "@clerk/chrome-extension" + "@clerk/clerk-js" + "@clerk/dev-cli" + "@clerk/expo" + "@clerk/expo-passkeys" + "@clerk/express" + "@clerk/fastify" + "@clerk/localizations" + "@clerk/nextjs" + "@clerk/nuxt" + "@clerk/react" + "@clerk/react-router" + "@clerk/shared" + "@clerk/tanstack-react-start" + "@clerk/testing" + "@clerk/types" + "@clerk/ui" + "@clerk/upgrade" + "@clerk/vue" + ) + for pkg in "${packages[@]}"; do + version=$(pnpm view "$pkg" version 2>/dev/null || echo "not found") + printf "%-35s %s\n" "$pkg:" "$version" + done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61953716e78..571afb9bc06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,10 +64,10 @@ jobs: - name: Setup id: config uses: ./.github/actions/init-blacksmith - with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # with: + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Verify lockfile is deduped run: pnpm dedupe --check @@ -110,11 +110,11 @@ jobs: - name: Setup id: config uses: ./.github/actions/init-blacksmith - with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # with: + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Turbo Build run: pnpm turbo build $TURBO_ARGS --only @@ -154,21 +154,22 @@ jobs: - name: Setup id: config uses: ./.github/actions/init-blacksmith - with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # with: + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Check size using bundlewatch - run: pnpm turbo bundlewatch $TURBO_ARGS + continue-on-error: true env: BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} - CI_REPO_OWNER: ${{ vars.REPO_OWNER }} - CI_REPO_NAME: ${{ vars.REPO_NAME }} - CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_BRANCH: ${{ github.ref }} CI_BRANCH_BASE: refs/heads/main + CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + CI_REPO_NAME: ${{ vars.REPO_NAME }} + CI_REPO_OWNER: ${{ vars.REPO_OWNER }} + run: pnpm turbo bundlewatch $TURBO_ARGS - name: Lint packages using publint run: pnpm turbo lint:publint $TURBO_ARGS @@ -191,7 +192,7 @@ jobs: unit-tests: needs: [check-permissions, build-packages] if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} - name: Unit Tests (${{ matrix.node-version }}, ${{ matrix.filter-label }}${{ matrix.clerk-use-rq == 'true' && ', RQ' || '' }}) + name: Unit Tests (${{ matrix.filter-label }}${{ matrix.clerk-use-rq == 'true' && ', RQ' || '' }}) permissions: contents: read actions: write # needed for actions/upload-artifact @@ -228,12 +229,12 @@ jobs: id: config uses: ./.github/actions/init-blacksmith with: - # Ensures that all builds are cached appropriately with a consistent run name `Unit Tests (18)`. + # Ensures that all builds are cached appropriately with a consistent run name `Unit Tests (20)`. node-version: ${{ matrix.node-version }} - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Rebuild @clerk/shared with CLERK_USE_RQ=true if: ${{ matrix.clerk-use-rq == 'true' }} @@ -264,7 +265,7 @@ jobs: run: | # Only run Typedoc tests for one matrix version and main test run if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then - pnpm test:typedoc + pnpm turbo run //#test:typedoc fi env: NODE_VERSION: ${{ matrix.node-version }} @@ -279,7 +280,8 @@ jobs: retention-days: 5 integration-tests: - needs: [check-permissions, build-packages] + # needs: [check-permissions, build-packages] + needs: [check-permissions] if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} name: Integration Tests (${{ matrix.test-name }}, ${{ matrix.test-project }}${{ matrix.next-version && format(', {0}', matrix.next-version) || '' }}${{ matrix.clerk-use-rq == 'true' && ', RQ' || '' }}) permissions: @@ -299,14 +301,12 @@ jobs: "generic", "express", "ap-flows", - "elements", "localhost", "sessions", "sessions:staging", "handshake", "handshake:staging", "astro", - "expo-web", "tanstack-react-start", "vue", "nuxt", @@ -327,16 +327,12 @@ jobs: - test-name: "machine" test-project: "chrome" clerk-use-rq: "true" - - test-name: "nextjs" - test-project: "chrome" - next-version: "14" - test-name: "nextjs" test-project: "chrome" next-version: "15" - clerk-use-rq: "false" - test-name: "nextjs" test-project: "chrome" - next-version: "15" + next-version: "16" clerk-use-rq: "true" - test-name: "nextjs" test-project: "chrome" @@ -359,9 +355,9 @@ jobs: id: config uses: ./.github/actions/init-blacksmith with: - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} playwright-enabled: true - name: Verify jq is installed @@ -377,7 +373,9 @@ jobs: id: task-status env: E2E_APP_CLERK_JS_DIR: ${{runner.temp}} - E2E_CLERK_VERSION: "latest" + E2E_APP_CLERK_UI_DIR: ${{runner.temp}} + E2E_CLERK_JS_VERSION: "latest" + E2E_CLERK_UI_VERSION: "latest" E2E_NEXTJS_VERSION: ${{ matrix.next-version }} E2E_PROJECT: ${{ matrix.test-project }} INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} @@ -407,12 +405,16 @@ jobs: env: CLERK_USE_RQ: true + - name: Version packages for snapshot + if: ${{ steps.task-status.outputs.affected == '1' }} + run: npm run version-packages:snapshot ci + - name: Verdaccio if: ${{ steps.task-status.outputs.affected == '1' }} uses: ./.github/actions/verdaccio with: publish-cmd: | - if [ "$(pnpm config get registry)" = "https://registry.npmjs.org/" ]; then echo 'Error: Using default registry' && exit 1; else CLERK_USE_RQ=${{ matrix.clerk-use-rq }} pnpm turbo build $TURBO_ARGS --only && pnpm changeset publish --no-git-tag; fi + if [ "$(pnpm config get registry)" = "https://registry.npmjs.org/" ]; then echo 'Error: Using default registry' && exit 1; else ${{ matrix.clerk-use-rq == 'true' && 'CLERK_USE_RQ=true' || '' }} pnpm turbo build $TURBO_ARGS --only && pnpm changeset publish --no-git-tag --tag latest; fi - name: Edit .npmrc [link-workspace-packages=false] run: sed -i -E 's/link-workspace-packages=(deep|true)/link-workspace-packages=false/' .npmrc @@ -425,6 +427,8 @@ jobs: pnpm config set minimum-release-age-exclude @clerk/* pnpm add @clerk/backend + # Install published packages from Verdaccio to test against real npm install scenarios + # rather than local monorepo builds. Validates package structure, dependencies, and entry points. - name: Install @clerk/clerk-js in os temp if: ${{ steps.task-status.outputs.affected == '1' }} working-directory: ${{runner.temp}} @@ -434,6 +438,15 @@ jobs: pnpm config set minimum-release-age-exclude @clerk/* pnpm add @clerk/clerk-js + - name: Install @clerk/ui in os temp + if: ${{ steps.task-status.outputs.affected == '1' }} + working-directory: ${{runner.temp}} + run: | + mkdir clerk-ui && cd clerk-ui + pnpm init + pnpm config set minimum-release-age-exclude @clerk/* + pnpm add @clerk/ui + - name: Copy components @clerk/astro if: ${{ matrix.test-name == 'astro' }} run: cd packages/astro && pnpm copy:components @@ -467,14 +480,16 @@ jobs: timeout-minutes: 25 run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS env: + E2E_DEBUG: "1" E2E_APP_CLERK_JS_DIR: ${{runner.temp}} - E2E_CLERK_VERSION: "latest" + E2E_APP_CLERK_UI_DIR: ${{runner.temp}} + E2E_CLERK_JS_VERSION: "latest" + E2E_CLERK_UI_VERSION: "latest" E2E_NEXTJS_VERSION: ${{ matrix.next-version }} E2E_PROJECT: ${{ matrix.test-project }} E2E_CLERK_ENCRYPTION_KEY: ${{ matrix.clerk-encryption-key }} CLERK_USE_RQ: ${{ matrix.clerk-use-rq }} INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} - MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }} NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} @@ -510,10 +525,10 @@ jobs: with: turbo-enabled: true node-version: 22 - turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - turbo-summarize: ${{ env.TURBO_SUMMARIZE }} - turbo-team: ${{ vars.TURBO_TEAM }} - turbo-token: ${{ secrets.TURBO_TOKEN }} + # turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + # turbo-summarize: ${{ env.TURBO_SUMMARIZE }} + # turbo-team: ${{ vars.TURBO_TEAM }} + # turbo-token: ${{ secrets.TURBO_TOKEN }} - name: Publish with pkg-pr-new run: pnpm run build && pnpx pkg-pr-new@${{ vars.PKG_PR_NEW_VERSION || '0.0.49' }} publish --compact --pnpm './packages/*' diff --git a/.github/workflows/nightly-checks.yml b/.github/workflows/nightly-checks.yml index b8edac5a23a..79ea94e5fdf 100644 --- a/.github/workflows/nightly-checks.yml +++ b/.github/workflows/nightly-checks.yml @@ -2,7 +2,7 @@ name: Nightly upstream tests on: workflow_dispatch: schedule: - - cron: '0 7 * * *' + - cron: "0 7 * * *" jobs: integration-tests: @@ -12,7 +12,7 @@ jobs: strategy: matrix: - test-name: ['nextjs'] + test-name: ["nextjs"] steps: - name: Checkout Repo @@ -44,6 +44,10 @@ jobs: working-directory: ${{runner.temp}} run: mkdir clerk-js && cd clerk-js && pnpm init && pnpm add @clerk/clerk-js + - name: Install @clerk/ui in os temp + working-directory: ${{runner.temp}} + run: mkdir clerk-ui && cd clerk-ui && pnpm init && pnpm add @clerk/ui + - name: Run Integration Tests id: integration_tests continue-on-error: true @@ -59,13 +63,14 @@ jobs: echo "exit_code=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT env: E2E_APP_CLERK_JS_DIR: ${{runner.temp}} - E2E_CLERK_VERSION: 'latest' - E2E_NEXTJS_VERSION: 'canary' - E2E_NPM_FORCE: 'true' - E2E_REACT_DOM_VERSION: '19.1.0' - E2E_REACT_VERSION: '19.1.0' + E2E_APP_CLERK_UI_DIR: ${{runner.temp}} + E2E_CLERK_JS_VERSION: "latest" + E2E_CLERK_UI_VERSION: "latest" + E2E_NEXTJS_VERSION: "canary" + E2E_NPM_FORCE: "true" + E2E_REACT_DOM_VERSION: "19.2.1" + E2E_REACT_VERSION: "19.2.1" INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} - MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }} # Upload test artifacts if tests failed - name: Upload Test Artifacts @@ -78,6 +83,7 @@ jobs: integration/test-results/ integration/.next/ ${{runner.temp}}/clerk-js/node_modules/ + ${{runner.temp}}/clerk-ui/node_modules/ retention-days: 7 - name: Report Status @@ -85,8 +91,8 @@ jobs: uses: ravsamhq/notify-slack-action@v1 with: status: ${{ steps.integration_tests.outputs.exit_code == '0' && 'success' || 'failure' }} - notify_when: 'failure' - notification_title: 'Integration Test Failure - ${{ matrix.test-name }}' + notify_when: "failure" + notification_title: "Integration Test Failure - ${{ matrix.test-name }}" message_format: | *Job:* ${{ github.workflow }} (${{ matrix.test-name }}) *Status:* ${{ steps.integration_tests.outputs.exit_code == '0' && 'Success' || 'Failed' }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index e1bfb68d1c9..b2b1ea1182a 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -77,7 +77,7 @@ jobs: turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} turbo-team: ${{ vars.TURBO_TEAM }} turbo-token: ${{ secrets.TURBO_TOKEN }} - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Build packages run: pnpm turbo build $TURBO_ARGS @@ -101,12 +101,17 @@ jobs: run: | cp -r $GITHUB_WORKSPACE/packages/clerk-js/dist $FULL_TMP_FOLDER/public/clerk-js + - name: Copy ui/dist/browser into public/clerk-ui of test site + run: | + cp -r $GITHUB_WORKSPACE/packages/ui/dist/browser $FULL_TMP_FOLDER/public/clerk-ui + - name: Build with Vercel run: | cd $FULL_TMP_FOLDER vercel build --yes env: NEXT_PUBLIC_CLERK_JS_URL: /clerk-js/clerk.browser.js + NEXT_PUBLIC_CLERK_UI_URL: /clerk-ui/ui.browser.js - name: Deploy to Vercel (prebuilt) id: vercel-deploy diff --git a/.github/workflows/release-canary-core-3.yml b/.github/workflows/release-canary-core-3.yml new file mode 100644 index 00000000000..054f75ab1b6 --- /dev/null +++ b/.github/workflows/release-canary-core-3.yml @@ -0,0 +1,73 @@ +name: canary-core3 release +run-name: canary-core3 release from ${{ github.ref_name }} + +on: + push: + branches: + - vincent-and-the-doctor + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + canary-core3-release: + if: ${{ github.repository == 'clerk/javascript' }} + runs-on: ${{ vars.RUNNER_NORMAL || 'ubuntu-latest' }} + timeout-minutes: ${{ vars.TIMEOUT_MINUTES_NORMAL && fromJSON(vars.TIMEOUT_MINUTES_NORMAL) || 10 }} + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + TURBO_CACHE: remote:rw + permissions: + contents: read + id-token: write + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup + id: config + uses: ./.github/actions/init + with: + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + turbo-team: ${{ vars.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + playwright-enabled: true # Must be present to enable caching on branched workflows + registry-url: "https://registry.npmjs.org" + + - name: Version packages for canary-core3 + id: version-packages + run: pnpm version-packages:canary-core3 | tail -1 >> "$GITHUB_OUTPUT" + + - name: Build release + if: steps.version-packages.outputs.success == '1' + run: pnpm turbo build $TURBO_ARGS + + - name: canary-core3 release + if: steps.version-packages.outputs.success == '1' + run: pnpm release:canary-core3 + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true + + - name: Trigger workflows on related repos + if: steps.version-packages.outputs.success == '1' + uses: actions/github-script@v7 + with: + result-encoding: string + retries: 3 + retry-exempt-status-codes: 400,401 + github-token: ${{ secrets.CLERK_COOKIE_PAT }} + script: | + const clerkjsVersion = require('./packages/clerk-js/package.json').version; + const clerkUiVersion = require('./packages/ui/package.json').version; + const nextjsVersion = require('./packages/nextjs/package.json').version; + + github.rest.actions.createWorkflowDispatch({ + owner: 'clerk', + repo: 'sdk-infra-workers', + workflow_id: 'update-pkg-versions.yml', + ref: 'main', + inputs: { clerkjsVersion, clerkUiVersion } + }) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bb4c2020e7..515a0803f91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,15 @@ jobs: fetch-depth: 0 show-progress: false + - name: Safety check - Prevent production releases on canary-core3 branch + run: | + if [ -f ".canary-core3-branch" ]; then + echo "❌ ERROR: This branch is marked for canary-core3 releases only!" + echo "Production releases (latest tag) are not allowed on this branch." + echo "If you need to make a production release, use the main production branch." + exit 1 + fi + - name: Setup id: config uses: ./.github/actions/init diff --git a/.gitignore b/.gitignore index d6d4562bddf..7b44af4edf8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ out **/dist/* **/build/* packages/*/dist/** +**/.pnpm-store/** # dependencies node_modules diff --git a/.jit/config.yml b/.jit/config.yml index 0381e46946f..1055ef4dbfc 100644 --- a/.jit/config.yml +++ b/.jit/config.yml @@ -17,9 +17,6 @@ folders: - path: /packages/react exclude: - ./**/*.test.ts - - path: /packages/remix - exclude: - - ./**/*.test.ts - path: /packages/shared exclude: - ./**/*.test.ts diff --git a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap deleted file mode 100644 index d0d845d9b2a..00000000000 --- a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap +++ /dev/null @@ -1,291 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Typedoc output > should have a deliberate file structure 1`] = ` -[ - "types/act-claim.mdx", - "types/act-jwt-claim.mdx", - "types/active-session-resource.mdx", - "types/add-payment-method-params.mdx", - "types/billing-checkout-json.mdx", - "types/billing-checkout-resource.mdx", - "types/billing-checkout-totals-json.mdx", - "types/billing-checkout-totals.mdx", - "types/billing-initialized-payment-method-json.mdx", - "types/billing-initialized-payment-method-resource.mdx", - "types/billing-money-amount-json.mdx", - "types/billing-money-amount.mdx", - "types/billing-namespace.mdx", - "types/billing-payer-json.mdx", - "types/billing-payer-methods.mdx", - "types/billing-payer-resource-type.mdx", - "types/billing-payer-resource.mdx", - "types/billing-payment-charge-type.mdx", - "types/billing-payment-json.mdx", - "types/billing-payment-method-json.mdx", - "types/billing-payment-method-resource.mdx", - "types/billing-payment-method-status.mdx", - "types/billing-payment-resource.mdx", - "types/billing-payment-status.mdx", - "types/billing-plan-json.mdx", - "types/billing-plan-resource.mdx", - "types/billing-statement-group-json.mdx", - "types/billing-statement-group.mdx", - "types/billing-statement-json.mdx", - "types/billing-statement-resource.mdx", - "types/billing-statement-status.mdx", - "types/billing-statement-totals-json.mdx", - "types/billing-statement-totals.mdx", - "types/billing-subscription-item-json.mdx", - "types/billing-subscription-item-resource.mdx", - "types/billing-subscription-json.mdx", - "types/billing-subscription-plan-period.mdx", - "types/billing-subscription-resource.mdx", - "types/billing-subscription-status.mdx", - "types/cancel-subscription-params.mdx", - "types/check-authorization-fn.mdx", - "types/check-authorization-from-session-claims.mdx", - "types/check-authorization-params-from-session-claims.mdx", - "types/check-authorization-with-custom-permissions.mdx", - "types/clerk-api-error.mdx", - "types/clerk-api-response-error.mdx", - "types/clerk-host-router.mdx", - "types/clerk-jwt-claims.mdx", - "types/clerk-paginated-response.mdx", - "types/clerk-pagination-params.mdx", - "types/clerk-pagination-request.mdx", - "types/clerk-resource.mdx", - "types/clerk-status.mdx", - "types/clerk.mdx", - "types/confirm-checkout-params.mdx", - "types/create-checkout-params.mdx", - "types/create-organization-params.mdx", - "types/deleted-object-resource.mdx", - "types/element-object-key.mdx", - "types/elements-config.mdx", - "types/errors.mdx", - "types/experimental_checkout-button-props.mdx", - "types/experimental_plan-details-button-props.mdx", - "types/experimental_subscription-details-button-props.mdx", - "types/feature-json.mdx", - "types/feature-resource.mdx", - "types/field-error.mdx", - "types/field-errors.mdx", - "types/for-payer-type.mdx", - "types/get-payment-attempts-params.mdx", - "types/get-payment-methods-params.mdx", - "types/get-plans-params.mdx", - "types/get-statements-params.mdx", - "types/get-subscription-params.mdx", - "types/get-token.mdx", - "types/id-selectors.mdx", - "types/initialize-payment-method-params.mdx", - "types/internal_checkout-props.mdx", - "types/internal_plan-details-props.mdx", - "types/internal_subscription-details-props.mdx", - "types/jwt-claims.mdx", - "types/jwt-header.mdx", - "types/legacy-redirect-props.mdx", - "types/localization-resource.mdx", - "types/make-default-payment-method-params.mdx", - "types/multi-domain-and-or-proxy.mdx", - "types/organization-custom-role-key.mdx", - "types/organization-domain-resource.mdx", - "types/organization-domain-verification-status.mdx", - "types/organization-enrollment-mode.mdx", - "types/organization-invitation-resource.mdx", - "types/organization-invitation-status.mdx", - "types/organization-membership-request-resource.mdx", - "types/organization-membership-resource.mdx", - "types/organization-permission-key.mdx", - "types/organization-resource.mdx", - "types/organization-suggestion-resource.mdx", - "types/organization-suggestion-status.mdx", - "types/organizations-jwt-claim.mdx", - "types/override.mdx", - "types/path-value.mdx", - "types/payment-gateway.mdx", - "types/pending-session-options.mdx", - "types/pending-session-resource.mdx", - "types/protect-props.mdx", - "types/record-to-path.mdx", - "types/redirect-options.mdx", - "types/remove-payment-method-params.mdx", - "types/reverification-config.mdx", - "types/saml-strategy.mdx", - "types/sdk-metadata.mdx", - "types/server-get-token-options.mdx", - "types/server-get-token.mdx", - "types/session-resource.mdx", - "types/session-status-claim.mdx", - "types/session-task.mdx", - "types/session-verification-level.mdx", - "types/session-verification-types.mdx", - "types/set-active-params.mdx", - "types/set-active.mdx", - "types/sign-in-future-resource.mdx", - "types/sign-in-resource.mdx", - "types/sign-in-signal-value.mdx", - "types/sign-out.mdx", - "types/sign-up-authenticate-with-metamask-params.mdx", - "types/sign-up-enterprise-connection-json.mdx", - "types/sign-up-enterprise-connection-resource.mdx", - "types/sign-up-future-resource.mdx", - "types/sign-up-resource.mdx", - "types/signed-in-session-resource.mdx", - "types/state-selectors.mdx", - "types/telemetry-log-entry.mdx", - "types/use-auth-return.mdx", - "types/use-session-list-return.mdx", - "types/use-session-return.mdx", - "types/use-sign-in-return.mdx", - "types/use-sign-up-return.mdx", - "types/use-user-return.mdx", - "types/user-organization-invitation-resource.mdx", - "types/user-resource.mdx", - "types/without.mdx", - "shared/api-url-from-publishable-key.mdx", - "shared/build-clerk-js-script-attributes.mdx", - "shared/build-publishable-key.mdx", - "shared/camel-to-snake.mdx", - "shared/clerk-api-error.mdx", - "shared/clerk-js-script-url.mdx", - "shared/clerk-runtime-error.mdx", - "shared/create-dev-or-staging-url-cache.mdx", - "shared/create-path-matcher.mdx", - "shared/deep-camel-to-snake.mdx", - "shared/deep-snake-to-camel.mdx", - "shared/deprecated-object-property.mdx", - "shared/derive-state.mdx", - "shared/extract-dev-browser-jwt-from-url.mdx", - "shared/fast-deep-merge-and-replace.mdx", - "shared/get-clerk-js-major-version-or-tag.mdx", - "shared/get-cookie-suffix.mdx", - "shared/get-env-variable.mdx", - "shared/get-non-undefined-values.mdx", - "shared/get-script-url.mdx", - "shared/get-suffixed-cookie-name.mdx", - "shared/icon-image-url.mdx", - "shared/in-browser.mdx", - "shared/is-browser-online.mdx", - "shared/is-clerk-runtime-error.mdx", - "shared/is-development-from-publishable-key.mdx", - "shared/is-development-from-secret-key.mdx", - "shared/is-ipv4-address.mdx", - "shared/is-production-from-publishable-key.mdx", - "shared/is-production-from-secret-key.mdx", - "shared/is-publishable-key.mdx", - "shared/is-staging.mdx", - "shared/is-truthy.mdx", - "shared/is-valid-browser-online.mdx", - "shared/is-valid-browser.mdx", - "shared/isomorphic-atob.mdx", - "shared/load-clerk-js-script.mdx", - "shared/local-storage-broadcast-channel.mdx", - "shared/pages-or-infinite-options.mdx", - "shared/paginated-hook-config.mdx", - "shared/paginated-resources.mdx", - "shared/parse-publishable-key.mdx", - "shared/read-json-file.mdx", - "shared/set-clerk-js-loading-error-package-name.mdx", - "shared/snake-to-camel.mdx", - "shared/titleize.mdx", - "shared/to-sentence.mdx", - "shared/use-clerk.mdx", - "shared/use-organization-list-params.mdx", - "shared/use-organization-list-return.mdx", - "shared/use-organization-list.mdx", - "shared/use-organization-params.mdx", - "shared/use-organization-return.mdx", - "shared/use-organization.mdx", - "shared/use-reverification.mdx", - "shared/use-session-list.mdx", - "shared/use-session.mdx", - "shared/use-user.mdx", - "shared/user-agent-is-robot.mdx", - "shared/version-selector.mdx", - "nextjs/auth.mdx", - "nextjs/build-clerk-props.mdx", - "nextjs/clerk-middleware-auth-object.mdx", - "nextjs/clerk-middleware-options.mdx", - "nextjs/clerk-middleware.mdx", - "nextjs/create-async-get-auth.mdx", - "nextjs/create-sync-get-auth.mdx", - "nextjs/current-user.mdx", - "nextjs/get-auth.mdx", - "nextjs/session-auth-with-redirect.mdx", - "clerk-react/api-keys.mdx", - "clerk-react/checkout-button-props.mdx", - "clerk-react/checkout-button.mdx", - "clerk-react/clerk-provider-props.mdx", - "clerk-react/plan-details-button-props.mdx", - "clerk-react/plan-details-button.mdx", - "clerk-react/protect.mdx", - "clerk-react/redirect-to-create-organization.mdx", - "clerk-react/redirect-to-organization-profile.mdx", - "clerk-react/redirect-to-user-profile.mdx", - "clerk-react/subscription-details-button-props.mdx", - "clerk-react/subscription-details-button.mdx", - "clerk-react/use-auth.mdx", - "clerk-react/use-clerk.mdx", - "clerk-react/use-organization-list.mdx", - "clerk-react/use-organization.mdx", - "clerk-react/use-reverification.mdx", - "clerk-react/use-session-list.mdx", - "clerk-react/use-session.mdx", - "clerk-react/use-sign-in-signal.mdx", - "clerk-react/use-sign-in.mdx", - "clerk-react/use-sign-up-signal.mdx", - "clerk-react/use-sign-up.mdx", - "clerk-react/use-user.mdx", - "backend/allowlist-identifier.mdx", - "backend/auth-object.mdx", - "backend/authenticate-request-options.mdx", - "backend/billing-payment-attempt-webhook-event-json.mdx", - "backend/billing-plan-json.mdx", - "backend/billing-plan.mdx", - "backend/billing-subscription-item-json.mdx", - "backend/billing-subscription-item-webhook-event-json.mdx", - "backend/billing-subscription-item.mdx", - "backend/billing-subscription-webhook-event-json.mdx", - "backend/billing-subscription.mdx", - "backend/client.mdx", - "backend/email-address.mdx", - "backend/external-account.mdx", - "backend/feature.mdx", - "backend/get-auth-fn-no-request.mdx", - "backend/get-auth-fn.mdx", - "backend/identification-link.mdx", - "backend/infer-auth-object-from-token-array.mdx", - "backend/infer-auth-object-from-token.mdx", - "backend/invitation-status.mdx", - "backend/invitation.mdx", - "backend/m2-m-token.mdx", - "backend/machine-scope.mdx", - "backend/machine-secret-key.mdx", - "backend/machine.mdx", - "backend/o-auth-application.mdx", - "backend/organization-invitation-status.mdx", - "backend/organization-invitation.mdx", - "backend/organization-membership-public-user-data.mdx", - "backend/organization-membership.mdx", - "backend/organization-sync-options.mdx", - "backend/organization-sync-target.mdx", - "backend/organization.mdx", - "backend/paginated-resource-response.mdx", - "backend/phone-number.mdx", - "backend/public-organization-data-json.mdx", - "backend/redirect-url.mdx", - "backend/saml-account.mdx", - "backend/saml-connection.mdx", - "backend/session-activity.mdx", - "backend/session.mdx", - "backend/user.mdx", - "backend/verification.mdx", - "backend/verify-machine-auth-token.mdx", - "backend/verify-token-options.mdx", - "backend/verify-token.mdx", - "backend/verify-webhook-options.mdx", - "backend/verify-webhook.mdx", - "backend/web3-wallet.mdx", -] -`; diff --git a/.typedoc/__tests__/file-structure.test.ts b/.typedoc/__tests__/file-structure.test.ts index a85c13fc087..010c72e267c 100644 --- a/.typedoc/__tests__/file-structure.test.ts +++ b/.typedoc/__tests__/file-structure.test.ts @@ -33,8 +33,8 @@ describe('Typedoc output', () => { expect(folders).toMatchInlineSnapshot(` [ "backend", - "clerk-react", "nextjs", + "react", "shared", ] `); diff --git a/canary-core3-branch b/canary-core3-branch new file mode 100644 index 00000000000..9dfa0b10923 --- /dev/null +++ b/canary-core3-branch @@ -0,0 +1,5 @@ +# Alpha v6 Development Branch +# This file marks this branch as an canary-core3 development branch. +# Production releases (latest tag) are disabled on this branch. +# Only canary-core3 releases are allowed. + diff --git a/commitlint.config.ts b/commitlint.config.ts index 17a8e933ac7..426d2f16fab 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -10,11 +10,12 @@ const getPackageNames = () => { const fullPath = join(packagesDir, entry); return statSync(fullPath).isDirectory(); }) - .map(dir => { + .flatMap(dir => { const packageJsonPath = join(packagesDir, dir, 'package.json'); try { const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - return packageJson.name.split('/').pop(); + const name = packageJson.name.split('/').pop() as string; + return [name, name.replace('clerk-', '')]; } catch { // Ignore directories without a package.json return null; @@ -27,7 +28,7 @@ const getPackageNames = () => { const Configuration = { extends: ['@commitlint/config-conventional'], rules: { - 'subject-case': [2, 'always', ['sentence-case']], + 'subject-case': [2, 'always', ['lower-case', 'sentence-case']], 'body-max-line-length': [1, 'always', '150'], 'scope-empty': [2, 'never'], 'scope-enum': [2, 'always', [...getPackageNames(), 'repo', 'release', 'e2e', '*']], diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 013f49829b7..ac5db7fddec 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -44,8 +44,7 @@ All packages of the monorepo are inside [packages](../packages). For package spe - [`@clerk/backend`](../packages/backend): Functionalities regarded as "core" for Clerk to operate with. _Authentication resolution, API Resources etc._ - [`@clerk/clerk-js`](../packages/clerk-js): Core JavaScript implementation used by Clerk in the browser. -- [`@clerk/clerk-react`](../packages/react) Clerk package for React applications. -- [`@clerk/types`](../packages/types): Main TypeScript typings for Clerk libraries. +- [`@clerk/react`](../packages/react) Clerk package for React applications. - Browse [packages](../packages) to see more Additionally there are packages which act as shared utilities or building blocks. @@ -186,16 +185,20 @@ To review your changes locally, you can run `pnpm run typedoc:generate` to gener Create a PR that includes your changes to any Typedoc comments. Once the PR has been merged and a release is published, a PR will [automatically](https://github.com/clerk/clerk-docs/blob/main/.github/workflows/typedoc.yml) be opened in `clerk-docs` to merge in the Typedoc changes. -Typedoc output is embedded in `clerk-docs` files with the `` component. For example, if you updated Typedoc comments for the `useAuth()` hook in `clerk/javascript`, you'll need to make sure that in `clerk-docs`, in the `/hooks/use-auth.mdx` file, there's a `` component linked to the `./clerk-typedoc/clerk-react/use-auth.mdx` file, like: +Typedoc output is embedded in `clerk-docs` files with the `` component. For example, if you updated Typedoc comments for the `useAuth()` hook in `clerk/javascript`, you'll need to make sure that in `clerk-docs`, in the `/hooks/use-auth.mdx` file, there's a `` component linked to the `./clerk-typedoc/react/use-auth.mdx` file, like: ```mdx - + ``` Read more about this in the [`clerk-docs` CONTRIBUTING.md](https://github.com/clerk/clerk-docs/blob/main/CONTRIBUTING.md#typedoc-). Then, to preview how the `` component renders, the `clerk-docs` PR will have a Vercel preview. Or to get local previews set up, see the [section in `clerk/clerk` about setting up local docs](https://github.com/clerk/clerk?tab=readme-ov-file#5-optional-set-up-local-docs). +### Experimental and internal APIs + +In some cases, we might need to add new methods to our publicly exposed APIs that are meant for internal use, or as experimental releases before the APIs are stabilized. For internal methods or properties, use the `__internal_` prefix. For experimental methods or properties that are attached to existing APIs, use the `__experimental_` prefix. For new exports, it is also acceptable to export from an `/experimental` subpath. Exports from `/experimental` are not covered by regular SemVer guarantees. + ## Opening a Pull Request 1. Search our repository for open or closed [Pull Requests](https://github.com/clerk/javascript/pulls) that relate to your submission. You don't want to duplicate effort. diff --git a/eslint.config.mjs b/eslint.config.mjs index 8881ed71ad6..8141869b880 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -89,6 +89,61 @@ const noNavigateUseClerk = { }, }; +const noUnstableMethods = { + meta: { + type: 'problem', + docs: { + description: 'Disallow methods or properties starting with `__unstable_`', + recommended: false, + }, + messages: { + noUnstable: + 'Do not define methods or properties starting with `__unstable_`. For internal APIs, use `__internal_`, for experimental APIs, use `__experimental_`.', + }, + schema: [], + }, + create(context) { + return { + MemberExpression(node) { + if ( + node.property.type === 'Identifier' && + typeof node.property.name === 'string' && + node.property.name.startsWith('__unstable_') + ) { + context.report({ + node: node.property, + messageId: 'noUnstable', + }); + } + }, + Property(node) { + if ( + node.key.type === 'Identifier' && + typeof node.key.name === 'string' && + node.key.name.startsWith('__unstable_') + ) { + context.report({ + node: node.key, + messageId: 'noUnstable', + }); + } + }, + MethodDefinition(node) { + if ( + node.key.type === 'Identifier' && + typeof node.key.name === 'string' && + node.key.name.startsWith('__unstable_') + ) { + context.report({ + node: node.key, + messageId: 'noUnstable', + }); + } + }, + }; + }, +}; + export default tseslint.config([ { name: 'repo/ignores', @@ -161,6 +216,11 @@ export default tseslint.config([ { name: 'repo/global', plugins: { + 'custom-rules': { + rules: { + 'no-unstable-methods': noUnstableMethods, + }, + }, 'simple-import-sort': pluginSimpleImportSort, 'unused-imports': pluginUnusedImports, turbo: pluginTurbo, @@ -176,6 +236,7 @@ export default tseslint.config([ }, }, rules: { + 'custom-rules/no-unstable-methods': 'error', 'no-label-var': 'error', 'no-undef-init': 'warn', 'no-restricted-imports': [ @@ -359,11 +420,13 @@ export default tseslint.config([ 'custom-rules': { rules: { 'no-navigate-useClerk': noNavigateUseClerk, + 'no-unstable-methods': noUnstableMethods, }, }, }, rules: { 'custom-rules/no-navigate-useClerk': 'error', + 'custom-rules/no-unstable-methods': 'error', }, }, { @@ -374,6 +437,29 @@ export default tseslint.config([ '@typescript-eslint/unbound-method': 'off', }, }, + { + name: 'packages/shared', + files: ['packages/shared/src/**/*'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['@clerk/shared', '@clerk/shared/*'], + message: + 'Do not import from @clerk/shared package exports within the package itself. Use the @/ alias or relative imports from source files instead (e.g., import from "@/types" or "../../types").', + }, + { + group: ['../../../*'], + message: + 'Relative imports should not traverse more than 2 levels up (../../). Use the @/ path alias instead (e.g., import from "@/types").', + }, + ], + }, + ], + }, + }, { name: 'packages/expo-passkeys', files: ['packages/expo-passkeys/src/**/*'], @@ -442,7 +528,7 @@ export default tseslint.config([ { definedTags: ['inline', 'unionReturnHeadings', 'displayFunctionSignature', 'paramExtension'], typed: false }, ], 'jsdoc/require-hyphen-before-param-description': 'warn', - 'jsdoc/require-description': 'warn', + 'jsdoc/require-description': 'off', 'jsdoc/require-description-complete-sentence': 'warn', 'jsdoc/require-param': ['warn', { ignoreWhenAllParamsMissing: true }], 'jsdoc/require-param-description': 'warn', @@ -455,6 +541,16 @@ export default tseslint.config([ ], }, }, + { + name: 'repo/jsdoc-internal', + files: ['packages/shared/src/**/internal/**/*.{ts,tsx}', 'packages/shared/src/**/*.{ts,tsx}'], + plugins: { + jsdoc: pluginJsDoc, + }, + rules: { + 'jsdoc/require-jsdoc': 'off', + }, + }, ...pluginYml.configs['flat/recommended'], { name: 'eslint-prettier', diff --git a/integration/.env.local.sample b/integration/.env.local.sample index 9505f3baa4c..8fdb1f0151c 100644 --- a/integration/.env.local.sample +++ b/integration/.env.local.sample @@ -1,4 +1,3 @@ -MAILSAC_API_KEY= VERCEL_PROJECT_ID= VERCEL_ORG_ID= VERCEL_TOKEN= diff --git a/integration/README.md b/integration/README.md index e6165c54dd8..ff565b2cce5 100644 --- a/integration/README.md +++ b/integration/README.md @@ -75,10 +75,10 @@ Below you can find code snippets for running tests in a specific manner, easily #### Keep temporary site -During E2E runs a temporary site is created in which the template is copied into. If you want to keep the site around, pass the `CLEANUP` environment variable: +During E2E runs a temporary site is created in which the template is copied into. If you want to keep the site around, pass the `E2E_CLEANUP` environment variable: ```shell -CLEANUP=0 pnpm test:integration:base +E2E_CLEANUP=0 pnpm test:integration:base ``` For all available environment variables, check the [`constants.ts`](../integration/constants.ts) file. @@ -364,7 +364,7 @@ Assuming you have a `react-parcel` template defined in `integration/templates`, .setName('react-parcel') .useTemplate(templates['react-parcel']) .setEnvFormatter('public', key => `${key}`) - .addDependency('@clerk/clerk-react', constants.E2E_CLERK_VERSION || clerkReactLocal); + .addDependency('@clerk/react', constants.E2E_CLERK_JS_VERSION || clerkReactLocal); ``` Here's what each thing is doing: @@ -578,7 +578,6 @@ Before writing tests, it's important to understand how Playwright handles test i > [!NOTE] > The test suite also uses these environment variables to run some tests: > -> - `MAILSAC_API_KEY`: Used for [Mailsac](https://mailsac.com/) to retrieve email codes and magic links from temporary email addresses. > - `VERCEL_PROJECT_ID`: Only required if you plan on running deployment tests locally. This is the Vercel project ID, and it points to an application created via the Vercel dashboard. The easiest way to get access to it is by linking a local app to the Vercel project using the Vercel CLI, and then copying the values from the `.vercel` directory. > - `VERCEL_ORG_ID`: The organization that owns the Vercel project. See above for more details. > - `VERCEL_TOKEN`: A personal access token. This corresponds to a real user running the deployment command. Attention: Be extra careful with this token as it can't be scoped to a single Vercel project, meaning that the token has access to every project in the account it belongs to. diff --git a/integration/constants.ts b/integration/constants.ts index 2369077fe84..227d6e267c3 100644 --- a/integration/constants.ts +++ b/integration/constants.ts @@ -29,10 +29,19 @@ export const constants = { */ E2E_APP_CLERK_JS_DIR: process.env.E2E_APP_CLERK_JS_DIR, /** - * If CLEANUP=0 is used, the .tmp_integration directory will not be deleted. + * Controls the URL the apps will load ui.browser.js from. + * If this is set, clerk-ui will not be served automatically from the test runner. + */ + E2E_APP_CLERK_UI: process.env.E2E_APP_CLERK_UI, + /** + * Controls the path where ui.browser.js is located on the disk. + */ + E2E_APP_CLERK_UI_DIR: process.env.E2E_APP_CLERK_UI_DIR, + /** + * If E2E_CLEANUP=0 is used, the .tmp_integration directory will not be deleted. * This is useful for debugging locally. */ - CLEANUP: !(process.env.CLEANUP === '0' || process.env.CLEANUP === 'false'), + E2E_CLEANUP: !(process.env.E2E_CLEANUP === '0' || process.env.E2E_CLEANUP === 'false'), DEBUG: process.env.DEBUG === 'true' || process.env.DEBUG === '1', /** * Used with E2E_APP_URL if the tests need to access BAPI. @@ -63,7 +72,11 @@ export const constants = { /** * The version of the dependency to use, controlled programmatically. */ - E2E_CLERK_VERSION: process.env.E2E_CLERK_VERSION, + E2E_CLERK_JS_VERSION: process.env.E2E_CLERK_JS_VERSION, + /** + * The version of the dependency to use, controlled programmatically. + */ + E2E_CLERK_UI_VERSION: process.env.E2E_CLERK_UI_VERSION, /** * Key used to encrypt request data for Next.js dynamic keys. * @ref https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys diff --git a/integration/models/application.ts b/integration/models/application.ts index c8a6b2df2aa..c4b2b059a92 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -1,5 +1,8 @@ import * as path from 'node:path'; +import { parsePublishableKey } from '@clerk/shared/keys'; +import { clerkSetup } from '@clerk/testing/playwright'; + import { awaitableTreekill, createLogger, fs, getPort, run, waitForIdleProcess, waitForServer } from '../scripts'; import type { ApplicationConfig } from './applicationConfig.js'; import type { EnvironmentConfig } from './environment.js'; @@ -46,6 +49,10 @@ export const application = ( const log = logger.child({ prefix: 'setup' }).info; await run(scripts.setup, { cwd: appDirPath, log }); state.completedSetup = true; + // Print all Clerk package versions (direct and transitive) + const clerkPackagesLog = logger.child({ prefix: 'clerk-packages' }).info; + clerkPackagesLog('Installed @clerk/* packages:'); + await run('pnpm list @clerk/* --depth 100', { cwd: appDirPath, log: clerkPackagesLog }); } }, dev: async (opts: { port?: number; manualStart?: boolean; detached?: boolean; serverUrl?: string } = {}) => { @@ -82,6 +89,36 @@ export const application = ( log(`Server started at ${runtimeServerUrl}, pid: ${proc.pid}`); cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL')); state.serverUrl = runtimeServerUrl; + + // Setup Clerk testing tokens after the server is running + if (state.env) { + try { + const publishableKey = state.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); + const secretKey = state.env.privateVariables.get('CLERK_SECRET_KEY'); + const apiUrl = state.env.privateVariables.get('CLERK_API_URL'); + + if (publishableKey && secretKey) { + const { instanceType, frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); + + if (instanceType !== 'development') { + log('Skipping clerkSetup for non-development instance'); + } else { + await clerkSetup({ + publishableKey, + frontendApiUrl, + secretKey, + // @ts-expect-error apiUrl is not a typed option for clerkSetup, but it is accepted at runtime. + apiUrl, + dotenv: false, + }); + log('Clerk testing tokens setup complete'); + } + } + } catch (error) { + logger.warn('Failed to setup Clerk testing tokens:', error); + } + } + return { port, serverUrl: runtimeServerUrl, pid: proc.pid }; }, build: async () => { diff --git a/integration/models/stateFile.ts b/integration/models/stateFile.ts index e95eb0ec02f..e5713b422f4 100644 --- a/integration/models/stateFile.ts +++ b/integration/models/stateFile.ts @@ -32,6 +32,12 @@ type StateFile = Partial<{ * The PID is used to teardown the http-server after the tests are done. */ clerkJsHttpServerPid: number; + /** + * This prop describes the pid of the http server that serves the clerk-ui hotloaded lib. + * The http-server replaces the production clerk-ui delivery mechanism. + * The PID is used to teardown the http-server after the tests are done. + */ + clerkUiHttpServerPid: number; }>; const createStateFile = () => { @@ -83,6 +89,16 @@ const createStateFile = () => { return read().clerkJsHttpServerPid; }; + const setClerkUiHttpServerPid = (pid: number) => { + const json = read(); + json.clerkUiHttpServerPid = pid; + write(json); + }; + + const getClerkUiHttpServerPid = () => { + return read().clerkUiHttpServerPid; + }; + const debug = () => { const json = read(); console.log('state file', JSON.stringify(json, null, 2)); @@ -94,6 +110,8 @@ const createStateFile = () => { getStandAloneApp, setClerkJsHttpServerPid, getClerkJsHttpServerPid, + setClerkUiHttpServerPid, + getClerkUiHttpServerPid, addLongRunningApp, getLongRunningApps, debug, diff --git a/integration/presets/astro.ts b/integration/presets/astro.ts index 7995725b97b..e9e310bf5fc 100644 --- a/integration/presets/astro.ts +++ b/integration/presets/astro.ts @@ -11,7 +11,7 @@ const astroNode = applicationConfig() .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') .addDependency('@clerk/astro', linkPackage('astro')) - .addDependency('@clerk/types', linkPackage('types')) + .addDependency('@clerk/shared', linkPackage('shared')) .addDependency('@clerk/localizations', linkPackage('localizations')); const astroStatic = astroNode.clone().setName('astro-hybrid').useTemplate(templates['astro-hybrid']); diff --git a/integration/presets/custom-flows.ts b/integration/presets/custom-flows.ts index bda524479f6..236967404b8 100644 --- a/integration/presets/custom-flows.ts +++ b/integration/presets/custom-flows.ts @@ -1,4 +1,3 @@ -import { constants } from '../constants'; import { applicationConfig } from '../models/applicationConfig'; import { templates } from '../templates'; import { linkPackage } from './utils'; @@ -11,8 +10,9 @@ const reactVite = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') - .addDependency('@clerk/clerk-react', constants.E2E_CLERK_VERSION || linkPackage('react')) - .addDependency('@clerk/themes', constants.E2E_CLERK_VERSION || linkPackage('themes')); + .addDependency('@clerk/react', linkPackage('react')) + .addDependency('@clerk/shared', linkPackage('shared')) + .addDependency('@clerk/ui', linkPackage('ui')); export const customFlows = { reactVite, diff --git a/integration/presets/elements.ts b/integration/presets/elements.ts deleted file mode 100644 index fb95a2ce5dc..00000000000 --- a/integration/presets/elements.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { constants } from '../constants'; -import { applicationConfig } from '../models/applicationConfig'; -import { templates } from '../templates'; -import { linkPackage } from './utils'; - -const nextAppRouter = applicationConfig() - .setName('elements-next') - .useTemplate(templates['elements-next']) - .setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`) - .addScript('setup', 'pnpm install') - .addScript('dev', 'pnpm dev') - .addScript('build', 'pnpm build') - .addScript('serve', 'pnpm start') - .addDependency('next', constants.E2E_NEXTJS_VERSION) - .addDependency('react', constants.E2E_REACT_VERSION) - .addDependency('react-dom', constants.E2E_REACT_DOM_VERSION) - .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || linkPackage('nextjs')) - .addDependency('@clerk/elements', constants.E2E_CLERK_VERSION || linkPackage('elements')); - -export const elements = { - nextAppRouter, -} as const; diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index d9a617739b3..43245f0093e 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -27,7 +27,8 @@ const base = environmentConfig() .setEnvVariable('public', 'CLERK_KEYLESS_DISABLED', true) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'); const withKeyless = base .clone() @@ -47,7 +48,8 @@ const sessionsProd1 = base .setId('sessionsProd1') .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('sessions-prod-1').sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('sessions-prod-1').pk) - .setEnvVariable('public', 'CLERK_JS_URL', ''); + .setEnvVariable('public', 'CLERK_JS_URL', '') + .setEnvVariable('public', 'CLERK_UI_URL', ''); const withEmailCodes_destroy_client = withEmailCodes .clone() @@ -63,7 +65,9 @@ const withCustomRoles = base .clone() .setId('withCustomRoles') .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-custom-roles').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-custom-roles').pk); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-custom-roles').pk) + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'); const withReverification = base .clone() @@ -82,7 +86,8 @@ const withAPCore1ClerkLatest = environmentConfig() .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-codes').sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'); const withAPCore1ClerkV4 = environmentConfig() .setId('withAPCore1ClerkV4') @@ -95,7 +100,8 @@ const withAPCore2ClerkLatest = environmentConfig() .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('core-2-all-enabled').sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('core-2-all-enabled').pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_UI_URL', constants.E2E_APP_CLERK_UI || 'http://localhost:18212/ui.browser.js'); const withAPCore2ClerkV4 = environmentConfig() .setId('withAPCore2ClerkV4') @@ -161,9 +167,8 @@ const withSessionTasksResetPassword = base const withBillingJwtV2 = base .clone() .setId('withBillingJwtV2') - .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') - .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging').pk); + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing').sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing').pk); const withBilling = base .clone() diff --git a/integration/presets/expo.ts b/integration/presets/expo.ts index bf9806fce22..ad692fd2268 100644 --- a/integration/presets/expo.ts +++ b/integration/presets/expo.ts @@ -10,7 +10,7 @@ const expoWeb = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm start') - .addDependency('@clerk/clerk-expo', linkPackage('expo')); + .addDependency('@clerk/expo', linkPackage('expo')); export const expo = { expoWeb, diff --git a/integration/presets/express.ts b/integration/presets/express.ts index 8ca84ae40ae..3a0ee82f392 100644 --- a/integration/presets/express.ts +++ b/integration/presets/express.ts @@ -1,4 +1,3 @@ -import { constants } from '../constants'; import { applicationConfig } from '../models/applicationConfig'; import { templates } from '../templates'; import { linkPackage } from './utils'; @@ -11,8 +10,9 @@ const vite = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm start') - .addDependency('@clerk/express', constants.E2E_CLERK_VERSION || linkPackage('express')) - .addDependency('@clerk/clerk-js', constants.E2E_CLERK_VERSION || linkPackage('clerk-js')); + .addDependency('@clerk/express', linkPackage('express')) + .addDependency('@clerk/clerk-js', linkPackage('clerk-js')) + .addDependency('@clerk/ui', linkPackage('ui')); export const express = { vite, diff --git a/integration/presets/index.ts b/integration/presets/index.ts index 5048abef518..0d53bfaeaa5 100644 --- a/integration/presets/index.ts +++ b/integration/presets/index.ts @@ -1,6 +1,5 @@ import { astro } from './astro'; import { customFlows } from './custom-flows'; -import { elements } from './elements'; import { envs, instanceKeys } from './envs'; import { expo } from './expo'; import { express } from './express'; @@ -19,7 +18,6 @@ export const appConfigs = { longRunningApps: createLongRunningApps(), next, react, - elements, expo, astro, tanstack, diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index a5acc533fc6..6a324675730 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -1,7 +1,6 @@ import type { LongRunningApplication } from '../models/longRunningApplication'; import { longRunningApplication } from '../models/longRunningApplication'; import { astro } from './astro'; -import { elements } from './elements'; import { envs } from './envs'; import { expo } from './expo'; import { express } from './express'; @@ -43,15 +42,7 @@ export const createLongRunningApps = () => { * Billing apps */ { id: 'withBillingJwtV2.next.appRouter', config: next.appRouter, env: envs.withBillingJwtV2 }, - { id: 'withBilling.next.appRouter', config: next.appRouter, env: envs.withBilling }, { id: 'withBillingJwtV2.vue.vite', config: vue.vite, env: envs.withBillingJwtV2 }, - { id: 'withBilling.vue.vite', config: vue.vite, env: envs.withBilling }, - - /** - * Machine auth apps - */ - { id: 'withMachine.express.vite', config: express.vite, env: envs.withAPIKeys }, - { id: 'withMachine.next.appRouter', config: next.appRouter, env: envs.withAPIKeys }, /** * Vite apps - basic flows @@ -69,14 +60,12 @@ export const createLongRunningApps = () => { /** * Various apps - basic flows */ - { id: 'withBilling.astro.node', config: astro.node, env: envs.withBilling }, { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles }, { id: 'astro.static.withCustomRoles', config: astro.static, env: envs.withCustomRoles }, { id: 'expo.expo-web', config: expo.expoWeb, env: envs.withEmailCodes }, { id: 'nuxt.node', config: nuxt.node, env: envs.withCustomRoles }, { id: 'react-router.node', config: reactRouter.reactRouterNode, env: envs.withEmailCodes }, { id: 'express.vite.withEmailCodes', config: express.vite, env: envs.withEmailCodes }, - { id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes }, ] as const; const apps = configs.map(longRunningApplication); diff --git a/integration/presets/next.ts b/integration/presets/next.ts index e2397d2a236..bfa1966d957 100644 --- a/integration/presets/next.ts +++ b/integration/presets/next.ts @@ -14,9 +14,8 @@ const appRouter = applicationConfig() .addDependency('next', constants.E2E_NEXTJS_VERSION) .addDependency('react', constants.E2E_REACT_VERSION) .addDependency('react-dom', constants.E2E_REACT_DOM_VERSION) - .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || linkPackage('nextjs')) - .addDependency('@clerk/shared', linkPackage('shared')) - .addDependency('@clerk/types', linkPackage('types')); + .addDependency('@clerk/nextjs', constants.E2E_CLERK_JS_VERSION || linkPackage('nextjs')) + .addDependency('@clerk/shared', linkPackage('shared')); const appRouterTurbo = appRouter.clone().setName('next-app-router-turbopack').addScript('dev', 'pnpm dev'); diff --git a/integration/presets/nuxt.ts b/integration/presets/nuxt.ts index 72baab0b22d..a57d6cc4aaa 100644 --- a/integration/presets/nuxt.ts +++ b/integration/presets/nuxt.ts @@ -12,7 +12,7 @@ const nuxtNode = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') - .addDependency('@clerk/nuxt', constants.E2E_CLERK_VERSION || linkPackage('nuxt')) + .addDependency('@clerk/nuxt', constants.E2E_CLERK_JS_VERSION || linkPackage('nuxt')) .addDependency('@clerk/shared', linkPackage('shared')) .addDependency('@clerk/types', linkPackage('types')) .addDependency('@clerk/vue', linkPackage('vue')); diff --git a/integration/presets/react-router.ts b/integration/presets/react-router.ts index 6dc759ddae7..da2bf124c05 100644 --- a/integration/presets/react-router.ts +++ b/integration/presets/react-router.ts @@ -11,7 +11,7 @@ const reactRouterNode = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm start') - .addDependency('@clerk/react-router', constants.E2E_CLERK_VERSION || linkPackage('react-router')); + .addDependency('@clerk/react-router', constants.E2E_CLERK_JS_VERSION || linkPackage('react-router')); const reactRouterLibrary = applicationConfig() .setName('react-router-library') @@ -21,7 +21,7 @@ const reactRouterLibrary = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') - .addDependency('@clerk/react-router', constants.E2E_CLERK_VERSION || linkPackage('react-router')); + .addDependency('@clerk/react-router', constants.E2E_CLERK_JS_VERSION || linkPackage('react-router')); export const reactRouter = { reactRouterNode, diff --git a/integration/presets/react.ts b/integration/presets/react.ts index 06e14342827..863e17b106c 100644 --- a/integration/presets/react.ts +++ b/integration/presets/react.ts @@ -1,4 +1,3 @@ -import { constants } from '../constants'; import { applicationConfig } from '../models/applicationConfig'; import { templates } from '../templates'; import { linkPackage } from './utils'; @@ -11,8 +10,9 @@ const cra = applicationConfig() .addScript('dev', 'pnpm start') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm start') - .addDependency('@clerk/clerk-react', constants.E2E_CLERK_VERSION || linkPackage('react')) - .addDependency('@clerk/themes', constants.E2E_CLERK_VERSION || linkPackage('themes')); + .addDependency('@clerk/react', linkPackage('react')) + .addDependency('@clerk/shared', linkPackage('shared')) + .addDependency('@clerk/ui', linkPackage('ui')); const vite = cra .clone() diff --git a/integration/presets/utils.ts b/integration/presets/utils.ts index f7831c39663..21672d16b5b 100644 --- a/integration/presets/utils.ts +++ b/integration/presets/utils.ts @@ -1,8 +1,10 @@ import path from 'node:path'; -export function linkPackage(pkg: string) { +export function linkPackage(pkg: string, tag?: string) { // eslint-disable-next-line turbo/no-undeclared-env-vars if (process.env.CI === 'true') { + // In CI, use '*' to get the latest version from Verdaccio + // which will be the snapshot version we just published return '*'; } diff --git a/integration/presets/vue.ts b/integration/presets/vue.ts index b8d73168403..85272d2c836 100644 --- a/integration/presets/vue.ts +++ b/integration/presets/vue.ts @@ -11,7 +11,7 @@ const vite = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') - .addDependency('@clerk/vue', constants.E2E_CLERK_VERSION || linkPackage('vue')) + .addDependency('@clerk/vue', constants.E2E_CLERK_JS_VERSION || linkPackage('vue')) .addDependency('@clerk/localizations', linkPackage('localizations')); export const vue = { diff --git a/integration/scripts/clerkJsServer.ts b/integration/scripts/clerkJsServer.ts index 5e837f9a9cd..315b75cd6ef 100644 --- a/integration/scripts/clerkJsServer.ts +++ b/integration/scripts/clerkJsServer.ts @@ -1,67 +1,34 @@ /* eslint-disable turbo/no-undeclared-env-vars */ -import os from 'node:os'; import path from 'node:path'; -import { constants } from '../constants'; import { stateFile } from '../models/stateFile'; -import { awaitableTreekill, fs, waitForServer } from '.'; -import { run } from './run'; +import { getTempDir, killHttpServer, startHttpServer } from './httpServer'; -export const startClerkJsHttpServer = async () => { +export const startClerkJsHttpServer = async (): Promise => { if (process.env.E2E_APP_CLERK_JS) { return; } - if (!process.env.CI) { - await copyClerkJsToTempDir(); - } - return serveFromTempDir(); -}; - -export const killClerkJsHttpServer = async () => { - const clerkJsHttpServerPid = stateFile.getClerkJsHttpServerPid(); - if (clerkJsHttpServerPid) { - console.log('Killing clerkJsHttpServer', clerkJsHttpServerPid); - await awaitableTreekill(clerkJsHttpServerPid, 'SIGKILL'); - } -}; -// If we are running the tests locally, then clerk.browser.js should be built already -// so we simply copy it from packages/clerk to the same location as CICD would install it -const copyClerkJsToTempDir = async () => { - const clerkJsTempDir = getClerkJsTempDir(); - await fs.remove(clerkJsTempDir); - await fs.ensureDir(clerkJsTempDir); - const packagesClerkJsDistPath = path.join(process.cwd(), 'packages/clerk-js/dist'); - fs.copySync(packagesClerkJsDistPath, clerkJsTempDir); -}; + const clerkJsTempDir = getTempDir('clerk-js/node_modules/@clerk/clerk-js/dist', 'E2E_APP_CLERK_JS_DIR'); + const sourceDir = path.join(process.cwd(), 'packages/clerk-js/dist'); -const serveFromTempDir = async () => { - console.log('Serving clerkJs from temp dir'); - const port = 18211; - const serverUrl = `http://localhost:${port}`; - const now = Date.now(); - const stdoutFilePath = path.resolve(constants.TMP_DIR, `clerkJsHttpServer.${now}.log`); - const stderrFilePath = path.resolve(constants.TMP_DIR, `clerkJsHttpServer.${now}.err.log`); - const clerkJsTempDir = getClerkJsTempDir(); - const proc = run(`node_modules/.bin/http-server ${clerkJsTempDir} -d --gzip --cors -a localhost`, { - cwd: process.cwd(), - env: { PORT: port.toString() }, - detached: true, - stdout: fs.openSync(stdoutFilePath, 'a'), - stderr: fs.openSync(stderrFilePath, 'a'), + const { pid } = await startHttpServer({ + name: 'clerkJs', + port: 18211, + sourceDir, + targetTempDir: clerkJsTempDir, + envVarOverride: 'E2E_APP_CLERK_JS', + envVarDirOverride: 'E2E_APP_CLERK_JS_DIR', + shouldCopyInLocal: true, }); - stateFile.setClerkJsHttpServerPid(proc.pid); - await waitForServer(serverUrl, { log: console.log, maxAttempts: Infinity }); - console.log('clerk.browser.js is being served from', serverUrl); + + stateFile.setClerkJsHttpServerPid(pid); }; -// The location where the clerk.browser.js is served from -// For simplicity, on CICD we install `@clerk/clerk-js` on osTemp -// so the actual clerk.browser.file is at osTemp/clerk-js/node_modules/@clerk/clerk-js/dist -// Locally, it's the osTemp/clerk-js/node_modules/@clerk/clerk-js/dist -// You can override it by setting the `E2E_APP_CLERK_JS_DIR` env variable -const getClerkJsTempDir = () => { - const osTempDir = process.env.E2E_APP_CLERK_JS_DIR || os.tmpdir(); - return path.join(osTempDir, ...'clerk-js/node_modules/@clerk/clerk-js/dist'.split('/')); +export const killClerkJsHttpServer = async (): Promise => { + const clerkJsHttpServerPid = stateFile.getClerkJsHttpServerPid(); + if (clerkJsHttpServerPid) { + await killHttpServer(clerkJsHttpServerPid, 'clerkJs'); + } }; diff --git a/integration/scripts/clerkUiServer.ts b/integration/scripts/clerkUiServer.ts new file mode 100644 index 00000000000..b3af88e0a02 --- /dev/null +++ b/integration/scripts/clerkUiServer.ts @@ -0,0 +1,34 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ + +import path from 'node:path'; + +import { stateFile } from '../models/stateFile'; +import { getTempDir, killHttpServer, startHttpServer } from './httpServer'; + +export const startClerkUiHttpServer = async (): Promise => { + if (process.env.E2E_APP_CLERK_UI) { + return; + } + + const clerkUiTempDir = getTempDir('clerk-ui/node_modules/@clerk/ui/dist', 'E2E_APP_CLERK_UI_DIR'); + const sourceDir = path.join(process.cwd(), 'packages/ui/dist'); + + const { pid } = await startHttpServer({ + name: 'clerkUi', + port: 18212, + sourceDir, + targetTempDir: clerkUiTempDir, + envVarOverride: 'E2E_APP_CLERK_UI', + envVarDirOverride: 'E2E_APP_CLERK_UI_DIR', + shouldCopyInLocal: true, + }); + + stateFile.setClerkUiHttpServerPid(pid); +}; + +export const killClerkUiHttpServer = async (): Promise => { + const clerkUiHttpServerPid = stateFile.getClerkUiHttpServerPid(); + if (clerkUiHttpServerPid) { + await killHttpServer(clerkUiHttpServerPid, 'clerkUi'); + } +}; diff --git a/integration/scripts/httpServer.ts b/integration/scripts/httpServer.ts new file mode 100644 index 00000000000..7bcd8fb3abf --- /dev/null +++ b/integration/scripts/httpServer.ts @@ -0,0 +1,71 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ + +import os from 'node:os'; +import path from 'node:path'; + +import { constants } from '../constants'; +import { awaitableTreekill, fs, waitForServer } from '.'; +import { run } from './run'; + +type HttpServerConfig = { + name: string; + port: number; + sourceDir: string; + targetTempDir: string; + envVarOverride?: string; + envVarDirOverride?: string; + shouldCopyInLocal: boolean; +}; + +const copyToTempDir = async (sourceDir: string, targetTempDir: string): Promise => { + await fs.remove(targetTempDir); + await fs.ensureDir(targetTempDir); + fs.copySync(sourceDir, targetTempDir); +}; + +const serveFromTempDir = async (config: HttpServerConfig): Promise<{ pid: number; serverUrl: string }> => { + console.log(`Serving ${config.name} from temp dir`); + const serverUrl = `http://localhost:${config.port}`; + const now = Date.now(); + const stdoutFilePath = path.resolve(constants.TMP_DIR, `${config.name}HttpServer.${now}.log`); + const stderrFilePath = path.resolve(constants.TMP_DIR, `${config.name}HttpServer.${now}.err.log`); + + const proc = run(`node_modules/.bin/http-server ${config.targetTempDir} -d --gzip --cors -a localhost`, { + cwd: process.cwd(), + env: { PORT: config.port.toString() }, + detached: true, + stdout: fs.openSync(stdoutFilePath, 'a'), + stderr: fs.openSync(stderrFilePath, 'a'), + }); + + await waitForServer(serverUrl, { log: console.log, maxAttempts: Infinity }); + console.log(`${config.name} is being served from`, serverUrl); + + return { pid: proc.pid, serverUrl }; +}; + +export const startHttpServer = async (config: HttpServerConfig): Promise<{ pid: number; serverUrl: string }> => { + // Skip if override env var is provided + if (config.envVarOverride && process.env[config.envVarOverride]) { + return { pid: 0, serverUrl: process.env[config.envVarOverride] }; + } + + // In local development, copy files to temp directory + if (!process.env.CI && config.shouldCopyInLocal) { + await copyToTempDir(config.sourceDir, config.targetTempDir); + } + + return serveFromTempDir(config); +}; + +export const killHttpServer = async (pid: number, serverName: string): Promise => { + if (pid) { + console.log(`Killing ${serverName}HttpServer`, pid); + await awaitableTreekill(pid, 'SIGKILL'); + } +}; + +export const getTempDir = (basePath: string, envVarOverride?: string): string => { + const osTempDir = envVarOverride && process.env[envVarOverride] ? process.env[envVarOverride] : os.tmpdir(); + return path.join(osTempDir, ...basePath.split('/')); +}; diff --git a/integration/scripts/index.ts b/integration/scripts/index.ts index e87e998628b..ff301be7798 100644 --- a/integration/scripts/index.ts +++ b/integration/scripts/index.ts @@ -15,3 +15,5 @@ export * from './setup'; export * from './waitForServer'; export { awaitableTreekill } from './awaitableTreekill'; export { startClerkJsHttpServer, killClerkJsHttpServer } from './clerkJsServer'; +export { startClerkUiHttpServer, killClerkUiHttpServer } from './clerkUiServer'; +export { startHttpServer, killHttpServer, getTempDir } from './httpServer'; diff --git a/integration/templates/astro-hybrid/astro.config.mjs b/integration/templates/astro-hybrid/astro.config.mjs index 30ff739e8a3..8568979754d 100644 --- a/integration/templates/astro-hybrid/astro.config.mjs +++ b/integration/templates/astro-hybrid/astro.config.mjs @@ -4,7 +4,16 @@ import react from '@astrojs/react'; export default defineConfig({ output: 'hybrid', - integrations: [clerk(), react()], + integrations: [ + clerk({ + appearance: { + options: { + showOptionalFields: true, + }, + }, + }), + react(), + ], server: { port: Number(process.env.PORT), }, diff --git a/integration/templates/astro-node/astro.config.mjs b/integration/templates/astro-node/astro.config.mjs index 6b08d1babd3..41e54f926f8 100644 --- a/integration/templates/astro-node/astro.config.mjs +++ b/integration/templates/astro-node/astro.config.mjs @@ -10,7 +10,17 @@ export default defineConfig({ adapter: node({ mode: 'standalone', }), - integrations: [clerk(), react(), tailwind()], + integrations: [ + clerk({ + appearance: { + options: { + showOptionalFields: true, + }, + }, + }), + react(), + tailwind(), + ], server: { port: Number(process.env.PORT), }, diff --git a/integration/templates/astro-node/src/components/CustomUserButton.astro b/integration/templates/astro-node/src/components/CustomUserButton.astro index 7586c0db5ba..6eed2bea4a9 100644 --- a/integration/templates/astro-node/src/components/CustomUserButton.astro +++ b/integration/templates/astro-node/src/components/CustomUserButton.astro @@ -2,7 +2,7 @@ import { UserButton } from '@clerk/astro/components'; --- - + diff --git a/integration/templates/astro-node/src/layouts/react/Layout.astro b/integration/templates/astro-node/src/layouts/react/Layout.astro index 2bc68f059e2..41b878880e3 100644 --- a/integration/templates/astro-node/src/layouts/react/Layout.astro +++ b/integration/templates/astro-node/src/layouts/react/Layout.astro @@ -80,10 +80,7 @@ import { LanguagePicker } from '../../components/LanguagePicker'; - + diff --git a/integration/templates/custom-flows-react-vite/src/main.tsx b/integration/templates/custom-flows-react-vite/src/main.tsx index 7b170e17b18..f89e557a8dc 100644 --- a/integration/templates/custom-flows-react-vite/src/main.tsx +++ b/integration/templates/custom-flows-react-vite/src/main.tsx @@ -2,7 +2,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter, Route, Routes } from 'react-router'; import './index.css'; -import { ClerkProvider } from '@clerk/clerk-react'; +import { ClerkProvider } from '@clerk/react'; import { Home } from './routes/Home'; import { SignIn } from './routes/SignIn'; import { SignUp } from './routes/SignUp'; @@ -22,6 +22,12 @@ createRoot(document.getElementById('root')!).render( diff --git a/integration/templates/custom-flows-react-vite/src/routes/Protected.tsx b/integration/templates/custom-flows-react-vite/src/routes/Protected.tsx index 1f937b66941..6c326c87021 100644 --- a/integration/templates/custom-flows-react-vite/src/routes/Protected.tsx +++ b/integration/templates/custom-flows-react-vite/src/routes/Protected.tsx @@ -1,4 +1,4 @@ -import { useUser } from '@clerk/clerk-react'; +import { useUser } from '@clerk/react'; export function Protected() { const { user, isLoaded } = useUser(); diff --git a/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx b/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx index eb4ab0041f9..27eead90579 100644 --- a/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx +++ b/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx @@ -5,15 +5,14 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { useUser } from '@clerk/clerk-react'; -import { useSignInSignal } from '@clerk/clerk-react/experimental'; +import { useSignIn, useUser } from '@clerk/react'; import { useState } from 'react'; import { NavLink, useNavigate } from 'react-router'; type AvailableStrategy = 'email_code' | 'phone_code' | 'password' | 'reset_password_email_code'; export function SignIn({ className, ...props }: React.ComponentProps<'div'>) { - const { signIn, errors, fetchStatus } = useSignInSignal(); + const { signIn, errors, fetchStatus } = useSignIn(); const [selectedStrategy, setSelectedStrategy] = useState(null); const { isSignedIn } = useUser(); const navigate = useNavigate(); diff --git a/integration/templates/custom-flows-react-vite/src/routes/SignUp.tsx b/integration/templates/custom-flows-react-vite/src/routes/SignUp.tsx index ef74268e35a..b506c46ecb0 100644 --- a/integration/templates/custom-flows-react-vite/src/routes/SignUp.tsx +++ b/integration/templates/custom-flows-react-vite/src/routes/SignUp.tsx @@ -5,11 +5,11 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { useSignUpSignal } from '@clerk/clerk-react/experimental'; +import { useSignUp } from '@clerk/react'; import { NavLink, useNavigate } from 'react-router'; export function SignUp({ className, ...props }: React.ComponentProps<'div'>) { - const { signUp, errors, fetchStatus } = useSignUpSignal(); + const { signUp, errors, fetchStatus } = useSignUp(); const navigate = useNavigate(); const handleSubmit = async (formData: FormData) => { diff --git a/integration/templates/elements-next/.gitignore b/integration/templates/elements-next/.gitignore deleted file mode 100644 index cdbd42c5c32..00000000000 --- a/integration/templates/elements-next/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -package-lock.json diff --git a/integration/templates/elements-next/README.md b/integration/templates/elements-next/README.md deleted file mode 100644 index a7da5398280..00000000000 --- a/integration/templates/elements-next/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -pnpm dev -# or -yarn dev -# or -pnpm dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/integration/templates/elements-next/next.config.js b/integration/templates/elements-next/next.config.js deleted file mode 100644 index 954fac0d40b..00000000000 --- a/integration/templates/elements-next/next.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - eslint: { - ignoreDuringBuilds: true, - }, -}; - -module.exports = nextConfig; diff --git a/integration/templates/elements-next/package.json b/integration/templates/elements-next/package.json deleted file mode 100644 index 526697ee31f..00000000000 --- a/integration/templates/elements-next/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "elements-next", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "next build", - "dev": "next dev", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "@clerk/elements": "file:../../../packages/elements", - "@clerk/nextjs": "file:../../../packages/nextjs", - "@types/node": "^18.19.33", - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1", - "next": "^14.2.13", - "react": "18.3.1", - "react-dom": "18.3.1", - "typescript": "^5.7.3" - }, - "devDependencies": { - "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "tailwindcss": "^3.4.17" - }, - "engines": { - "node": ">=18.17.0" - } -} diff --git a/integration/templates/elements-next/postcss.config.js b/integration/templates/elements-next/postcss.config.js deleted file mode 100644 index 12a703d900d..00000000000 --- a/integration/templates/elements-next/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/integration/templates/elements-next/src/app/favicon.ico b/integration/templates/elements-next/src/app/favicon.ico deleted file mode 100644 index 718d6fea483..00000000000 Binary files a/integration/templates/elements-next/src/app/favicon.ico and /dev/null differ diff --git a/integration/templates/elements-next/src/app/globals.css b/integration/templates/elements-next/src/app/globals.css deleted file mode 100644 index ea46f6b7409..00000000000 --- a/integration/templates/elements-next/src/app/globals.css +++ /dev/null @@ -1,49 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); - font-family: - -apple-system, - BlinkMacSystemFont, - avenir next, - avenir, - segoe ui, - helvetica neue, - helvetica, - Cantarell, - Ubuntu, - roboto, - noto, - arial, - sans-serif; -} - -main { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 6rem; - min-height: 100vh; -} diff --git a/integration/templates/elements-next/src/app/layout.tsx b/integration/templates/elements-next/src/app/layout.tsx deleted file mode 100644 index 9e5b6a73819..00000000000 --- a/integration/templates/elements-next/src/app/layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import './globals.css'; - -import { ClerkProvider } from '@clerk/nextjs'; -import type { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'Clerk Elements - Next.js E2E', - description: 'Clerk Elements - Next.js E2E', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ); -} diff --git a/integration/templates/elements-next/src/app/otp/page.tsx b/integration/templates/elements-next/src/app/otp/page.tsx deleted file mode 100644 index 60447b7dc6f..00000000000 --- a/integration/templates/elements-next/src/app/otp/page.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client'; - -import * as Clerk from '@clerk/elements/common'; -import * as SignIn from '@clerk/elements/sign-in'; - -function clsx(...args: (string | undefined | Record)[]): string { - const classes: string[] = []; - - for (const arg of args) { - switch (typeof arg) { - case 'string': - classes.push(arg); - break; - case 'object': - for (const key in arg) { - if (arg[key]) { - classes.push(key); - } - } - break; - } - } - - return classes.join(' '); -} - -export default function OTP() { - return ( -
- - -
-

OTP Playground

-
- - Simple OTP Input - - - - Segmented OTP Input - { - return ( -
- {value} - {status === 'cursor' && ( -
-
-
- )} -
- ); - }} - /> - - - Segmented OTP Input (with props) - { - return ( -
- {value} - {status === 'cursor' && ( -
-
-
- )} -
- ); - }} - /> - - - -
- ); -} diff --git a/integration/templates/elements-next/src/app/page.tsx b/integration/templates/elements-next/src/app/page.tsx deleted file mode 100644 index dafd45e8d6e..00000000000 --- a/integration/templates/elements-next/src/app/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import { SignedIn, SignedOut, SignOutButton } from '@clerk/nextjs'; -import Link from 'next/link'; - -function Card({ children, title }: { children: React.ReactNode; title: string }) { - return ( -
-

{title}

- {children} -
- ); -} - -export default function Home() { - return ( -
-

Clerk Elements: Next.js E2E

-

- Kitchen sink template to test out Clerk Elements in Next.js App Router. -

-
- - -

signed-out-state

-
- -

signed-in-state

-
-
- -
    -
  • - - Sign-In - -
  • -
  • - - Sign-Up - -
  • -
  • - - OTP Playground - -
  • -
  • - - Password Validation - -
  • -
-
- - -

Not logged in.

-
- - - - - -
-
-
- ); -} diff --git a/integration/templates/elements-next/src/app/sign-in/[[...sign-in]]/page.tsx b/integration/templates/elements-next/src/app/sign-in/[[...sign-in]]/page.tsx deleted file mode 100644 index 71d8af7573d..00000000000 --- a/integration/templates/elements-next/src/app/sign-in/[[...sign-in]]/page.tsx +++ /dev/null @@ -1,347 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as Clerk from '@clerk/elements/common'; -import * as SignIn from '@clerk/elements/sign-in'; - -// password, phone_code, email_code, email_link, reset_password_email_code, but the rendered strategies are: -// password, email_code, reset_password_email_code, phone_code - -function Button({ children, ...props }: { children: React.ReactNode }) { - return ( - - ); -} - -export default function SignInPage() { - const [usePhone, setUsePhone] = React.useState(false); - - return ( -
-
- - -
-

Sign in to Clover

-
- - -
- - {usePhone ? 'Phone number' : 'Email or username'} - - -
- - -
- - - - - - -
-

Alternatively, sign in with these platforms

-
- - - Login with Google - -
-
-
- -
-

Use another method

-
- - - - - - - - - - - - -
-

Alternatively, sign in with these platforms

-
- - - Login with Google - -
-
-

- - Go back - -

-
- -
-

Forgot password?

-
- - - - -
-

Alternatively, sign in with these platforms

-
- - - Login with Google - -
-
-
- - -
-

Enter your password

-

- Welcome back -

-
- - -
- Password - - Forgot password? - -
- - -
- - - -
- -
-

Verify email code

-
- - - Email code - - - - - - -
- -
-

Verify email link

-
- - - Email link - - - - - - -
- -
-

Verify email code

-
- - - Email code - - - - - - -
- -
-

Verify phone code

-
- - - Phone code - - - - - - -
-
- - Use another method - -
-
- -
-

Reset your password

-
- - - New password - - - - - Confirm password - - - - - - -
-
-
-
- ); -} diff --git a/integration/templates/elements-next/src/app/sign-up/[[...sign-up]]/page.tsx b/integration/templates/elements-next/src/app/sign-up/[[...sign-up]]/page.tsx deleted file mode 100644 index 6ff8dd5e569..00000000000 --- a/integration/templates/elements-next/src/app/sign-up/[[...sign-up]]/page.tsx +++ /dev/null @@ -1,152 +0,0 @@ -'use client'; - -import * as Clerk from '@clerk/elements/common'; -import * as SignUp from '@clerk/elements/sign-up'; - -export default function SignUpPage() { - return ( -
- - -
-

Create an account

-
- -
- - Email - - - - - Password - - - - - Phone number (optional) - - - - - Username (optional) - - - -
- - Continue - -
- - - -
-

Verify email code

-
- - Email code - - - - - Continue - -
- -
-

Verify phone code

-
- - Phone code - - - - - Continue - -
-
- -
-

Continue registration

-
- - - Username - - - - - Continue - -
-
-
- ); -} diff --git a/integration/templates/elements-next/src/app/validate-password/page.tsx b/integration/templates/elements-next/src/app/validate-password/page.tsx deleted file mode 100644 index 869ea04794b..00000000000 --- a/integration/templates/elements-next/src/app/validate-password/page.tsx +++ /dev/null @@ -1,94 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as Clerk from '@clerk/elements/common'; -import * as SignIn from '@clerk/elements/sign-in'; - -export default function ValitePassword() { - const [hidden, setHidden] = React.useState(true); - - return ( -
- - -
-

Password Validation Playground

-

- Just to test out the{' '} - - password validation - {' '} - 🙃 -

-
- -
- Password - -
- - - {({ state, codes, message }) => ( -
-

Field State Props

- - - - - - - - - - - - - - - - - - - - - -
PropValue
State - {state} -
Codes - {JSON.stringify(codes)} -
Message - {message} -
-
- )} -
-
-
-
-
- ); -} diff --git a/integration/templates/elements-next/src/middleware.ts b/integration/templates/elements-next/src/middleware.ts deleted file mode 100644 index 545508cedc1..00000000000 --- a/integration/templates/elements-next/src/middleware.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clerkMiddleware } from '@clerk/nextjs/server'; -export default clerkMiddleware; - -export const config = { - matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], -}; diff --git a/integration/templates/elements-next/tailwind.config.js b/integration/templates/elements-next/tailwind.config.js deleted file mode 100644 index 5eaa3171157..00000000000 --- a/integration/templates/elements-next/tailwind.config.js +++ /dev/null @@ -1,18 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], - theme: { - extend: { - keyframes: { - 'caret-blink': { - '0%,70%,100%': { opacity: '1' }, - '20%,50%': { opacity: '0' }, - }, - }, - animation: { - 'caret-blink': 'caret-blink 1.25s ease-out infinite', - }, - }, - }, - plugins: [], -}; diff --git a/integration/templates/elements-next/tsconfig.json b/integration/templates/elements-next/tsconfig.json deleted file mode 100644 index eb0b41d94d5..00000000000 --- a/integration/templates/elements-next/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/integration/templates/expo-web/app/_layout.tsx b/integration/templates/expo-web/app/_layout.tsx index 95a03ee596d..60c238a6892 100644 --- a/integration/templates/expo-web/app/_layout.tsx +++ b/integration/templates/expo-web/app/_layout.tsx @@ -1,5 +1,5 @@ import { Stack, useRouter } from 'expo-router'; -import { ClerkLoaded, ClerkProvider } from '@clerk/clerk-expo'; +import { ClerkLoaded, ClerkProvider } from '@clerk/expo'; export default function RootLayout() { const router = useRouter(); @@ -8,6 +8,13 @@ export default function RootLayout() { router.push(to)} routerReplace={to => router.replace(to)} + clerkJSUrl={process.env.EXPO_PUBLIC_CLERK_JS_URL} + clerkUiUrl={process.env.EXPO_PUBLIC_CLERK_UI_URL} + appearance={{ + options: { + showOptionalFields: true, + }, + }} > diff --git a/integration/templates/expo-web/app/custom-sign-in.tsx b/integration/templates/expo-web/app/custom-sign-in.tsx index 3ca42a81d3c..e5dc5a10911 100644 --- a/integration/templates/expo-web/app/custom-sign-in.tsx +++ b/integration/templates/expo-web/app/custom-sign-in.tsx @@ -1,38 +1,25 @@ -import { useSignIn } from '@clerk/clerk-expo'; +import { useSignIn } from '@clerk/expo'; import { Link, useRouter } from 'expo-router'; import { Text, TextInput, Button, View } from 'react-native'; import React from 'react'; export default function Page() { - const { signIn, setActive, isLoaded } = useSignIn(); + const { signIn } = useSignIn(); const router = useRouter(); const [emailAddress, setEmailAddress] = React.useState(''); const [password, setPassword] = React.useState(''); const onSignInPress = React.useCallback(async () => { - if (!isLoaded) { - return; - } - - try { - const signInAttempt = await signIn.create({ - identifier: emailAddress, - password, + await signIn.password({ emailAddress, password }); + if (signIn.status === 'complete') { + await signIn.finalize({ + navigate: async () => { + router.replace('/'); + }, }); - - if (signInAttempt.status === 'complete') { - await setActive({ session: signInAttempt.createdSessionId }); - router.replace('/'); - } else { - // See https://clerk.com/docs/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(signInAttempt, null, 2)); - } - } catch (err: any) { - console.error(JSON.stringify(err, null, 2)); } - }, [isLoaded, emailAddress, password]); + }, [emailAddress, password]); return ( diff --git a/integration/templates/expo-web/app/custom-sign-up.tsx b/integration/templates/expo-web/app/custom-sign-up.tsx index a51d547d908..6368bc0d1cd 100644 --- a/integration/templates/expo-web/app/custom-sign-up.tsx +++ b/integration/templates/expo-web/app/custom-sign-up.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { TextInput, Button, View } from 'react-native'; -import { useSignUp } from '@clerk/clerk-expo'; +import { useSignUp } from '@clerk/expo'; import { useRouter } from 'expo-router'; export default function SignUpScreen() { - const { isLoaded, signUp, setActive } = useSignUp(); + const { signUp } = useSignUp(); const router = useRouter(); const [emailAddress, setEmailAddress] = React.useState(''); @@ -13,46 +13,19 @@ export default function SignUpScreen() { const [code, setCode] = React.useState(''); const onSignUpPress = async () => { - if (!isLoaded) { - return; - } - - try { - await signUp.create({ - emailAddress, - password, - }); - - await signUp.prepareEmailAddressVerification({ strategy: 'email_code' }); - - setPendingVerification(true); - } catch (err: any) { - // See https://clerk.com/docs/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)); - } + await signUp.password({ emailAddress, password }); + await signUp.verifications.sendEmailCode({ emailAddress }); + setPendingVerification(true); }; const onPressVerify = async () => { - if (!isLoaded) { - return; - } - - try { - const completeSignUp = await signUp.attemptEmailAddressVerification({ - code, + await signUp.verifications.verifyEmailCode({ code }); + if (signUp.status === 'complete') { + await signUp.finalize({ + navigate: async () => { + router.replace('/'); + }, }); - - if (completeSignUp.status === 'complete') { - await setActive({ session: completeSignUp.createdSessionId }); - router.replace('/'); - } else { - console.error(JSON.stringify(completeSignUp, null, 2)); - } - } catch (err: any) { - // See https://clerk.com/docs/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)); } }; diff --git a/integration/templates/expo-web/app/index.tsx b/integration/templates/expo-web/app/index.tsx index f43e714fac6..431bf8c209f 100644 --- a/integration/templates/expo-web/app/index.tsx +++ b/integration/templates/expo-web/app/index.tsx @@ -1,6 +1,6 @@ import { Text, View } from 'react-native'; -import { SignedIn, SignedOut } from '@clerk/clerk-expo'; -import { UserButton } from '@clerk/clerk-expo/web'; +import { SignedIn, SignedOut } from '@clerk/expo'; +import { UserButton } from '@clerk/expo/web'; export default function Index() { return ( diff --git a/integration/templates/expo-web/app/sign-in.tsx b/integration/templates/expo-web/app/sign-in.tsx index 240376991d9..d627d7781a7 100644 --- a/integration/templates/expo-web/app/sign-in.tsx +++ b/integration/templates/expo-web/app/sign-in.tsx @@ -1,5 +1,5 @@ import { Text, View } from 'react-native'; -import { SignIn } from '@clerk/clerk-expo/web'; +import { SignIn } from '@clerk/expo/web'; export default function Index() { return ( diff --git a/integration/templates/expo-web/metro.config.js b/integration/templates/expo-web/metro.config.js index 1874df5a11a..c0f9eee8d78 100644 --- a/integration/templates/expo-web/metro.config.js +++ b/integration/templates/expo-web/metro.config.js @@ -8,10 +8,10 @@ const path = require('node:path'); /** @type {() => string | undefined} */ const getClerkExpoPath = () => { - const clerkExpoPath = packageJson.dependencies['@clerk/clerk-expo']; + const clerkExpoPath = packageJson.dependencies['@clerk/expo']; if (clerkExpoPath?.startsWith('*')) { - const pathToModule = require.resolve('@clerk/clerk-expo'); + const pathToModule = require.resolve('@clerk/expo'); return pathToModule.replace('dist/index.js', ''); } @@ -51,8 +51,8 @@ if (clerkMonorepoPath) { // Explicitly map @clerk packages to their source locations // Point to the root of the package so Metro can properly resolve subpath exports config.resolver.extraNodeModules = { - '@clerk/clerk-react': path.resolve(clerkMonorepoPath, 'packages/react'), - '@clerk/clerk-expo': path.resolve(clerkMonorepoPath, 'packages/expo'), + '@clerk/react': path.resolve(clerkMonorepoPath, 'packages/react'), + '@clerk/expo': path.resolve(clerkMonorepoPath, 'packages/expo'), '@clerk/shared': path.resolve(clerkMonorepoPath, 'packages/shared'), '@clerk/types': path.resolve(clerkMonorepoPath, 'packages/types'), }; @@ -87,7 +87,7 @@ if (clerkMonorepoPath) { ]; // Custom resolver to handle package.json subpath exports for @clerk packages - // This enables Metro to resolve imports like '@clerk/clerk-react/internal' + // This enables Metro to resolve imports like '@clerk/react/internal' const originalResolveRequest = config.resolver.resolveRequest; config.resolver.resolveRequest = (context, moduleName, platform) => { // Check if this is a @clerk package with a subpath diff --git a/integration/templates/expo-web/package.json b/integration/templates/expo-web/package.json index d26d26f63d8..2aba208949d 100644 --- a/integration/templates/expo-web/package.json +++ b/integration/templates/expo-web/package.json @@ -15,7 +15,7 @@ "dependencies": { "@expo/vector-icons": "^14.0.2", "@react-navigation/native": "^6.0.2", - "expo": "~51.0.17", + "expo": "~53", "expo-constants": "~16.0.2", "expo-font": "~12.0.7", "expo-linking": "~6.3.1", @@ -24,9 +24,9 @@ "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.6", "expo-web-browser": "~13.0.3", - "react": "18.3.1", - "react-dom": "18.3.1", - "react-native": "0.74.3", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-native": "0.82.1", "react-native-gesture-handler": "~2.16.1", "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.9", diff --git a/integration/templates/express-vite/src/client/main.ts b/integration/templates/express-vite/src/client/main.ts index 2656c4e02df..bf19f46d7b7 100644 --- a/integration/templates/express-vite/src/client/main.ts +++ b/integration/templates/express-vite/src/client/main.ts @@ -1,10 +1,14 @@ import { Clerk } from '@clerk/clerk-js'; +import { ClerkUi } from '@clerk/ui/entry'; const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; document.addEventListener('DOMContentLoaded', async function () { const clerk = new Clerk(publishableKey); - await clerk.load(); + + await clerk.load({ + clerkUiCtor: ClerkUi, + }); if (clerk.isSignedIn) { document.getElementById('app')!.innerHTML = ` diff --git a/integration/templates/next-app-router-quickstart/package.json b/integration/templates/next-app-router-quickstart/package.json index cbda141d7fa..f03c8bd84da 100644 --- a/integration/templates/next-app-router-quickstart/package.json +++ b/integration/templates/next-app-router-quickstart/package.json @@ -18,6 +18,6 @@ "typescript": "^5.7.3" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" } } diff --git a/integration/templates/next-app-router-quickstart/src/app/layout.tsx b/integration/templates/next-app-router-quickstart/src/app/layout.tsx index 29ddd566bdb..411ba883c93 100644 --- a/integration/templates/next-app-router-quickstart/src/app/layout.tsx +++ b/integration/templates/next-app-router-quickstart/src/app/layout.tsx @@ -11,7 +11,13 @@ export const metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} diff --git a/integration/templates/next-app-router/package.json b/integration/templates/next-app-router/package.json index f419946d5e8..c2243548937 100644 --- a/integration/templates/next-app-router/package.json +++ b/integration/templates/next-app-router/package.json @@ -19,6 +19,6 @@ "typescript": "^5.7.3" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" } } diff --git a/integration/templates/next-app-router/src/app/(reverification)/actions.ts b/integration/templates/next-app-router/src/app/(reverification)/actions.ts index 54334730534..76750881672 100644 --- a/integration/templates/next-app-router/src/app/(reverification)/actions.ts +++ b/integration/templates/next-app-router/src/app/(reverification)/actions.ts @@ -1,7 +1,7 @@ 'use server'; import { auth, reverificationError } from '@clerk/nextjs/server'; -import { ReverificationConfig } from '@clerk/types'; +import type { ReverificationConfig } from '@clerk/shared/types'; const logUserIdActionReverification = async () => { const { userId, has } = await auth.protect(); diff --git a/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx b/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx index a7d7102bb50..9c937cc10fb 100644 --- a/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx +++ b/integration/templates/next-app-router/src/app/jwt-v2-organizations/(tests)/has-ssr/page.tsx @@ -3,7 +3,14 @@ import { SSR } from './client'; export default function Page() { return ( - + ); diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx index 2e56184f39d..42341d04adc 100644 --- a/integration/templates/next-app-router/src/app/layout.tsx +++ b/integration/templates/next-app-router/src/app/layout.tsx @@ -13,7 +13,8 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( {children}; + return ( + + {children} + + ); } diff --git a/integration/templates/nuxt-node/app/middleware/auth.global.js b/integration/templates/nuxt-node/app/middleware/auth.global.js index 8ecaf1bb3f5..0e6f082773b 100644 --- a/integration/templates/nuxt-node/app/middleware/auth.global.js +++ b/integration/templates/nuxt-node/app/middleware/auth.global.js @@ -1,12 +1,12 @@ export default defineNuxtRouteMiddleware(to => { const { userId } = useAuth(); - const isPublicPage = createRouteMatcher(['/sign-in']); - const isProtectedPage = createRouteMatcher(['/user']); + const isPublicPage = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)']); + const isProtectedPage = createRouteMatcher(['/user-profile(.*)']); // Is authenticated and trying to access a public page if (userId.value && isPublicPage(to)) { - return navigateTo('/user'); + return navigateTo('/user-profile'); } // Is not authenticated and trying to access a protected page diff --git a/integration/templates/nuxt-node/app/pages/hash/sign-in/[...slug].vue b/integration/templates/nuxt-node/app/pages/hash/sign-in/[...slug].vue new file mode 100644 index 00000000000..19c4b6f25bf --- /dev/null +++ b/integration/templates/nuxt-node/app/pages/hash/sign-in/[...slug].vue @@ -0,0 +1,3 @@ + diff --git a/integration/templates/nuxt-node/app/pages/sign-in.vue b/integration/templates/nuxt-node/app/pages/sign-in.vue deleted file mode 100644 index b9258533122..00000000000 --- a/integration/templates/nuxt-node/app/pages/sign-in.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/integration/templates/nuxt-node/app/pages/sign-in/[...slug].vue b/integration/templates/nuxt-node/app/pages/sign-in/[...slug].vue new file mode 100644 index 00000000000..8a075773a66 --- /dev/null +++ b/integration/templates/nuxt-node/app/pages/sign-in/[...slug].vue @@ -0,0 +1,3 @@ + diff --git a/integration/templates/nuxt-node/app/pages/sign-up/[...slug].vue b/integration/templates/nuxt-node/app/pages/sign-up/[...slug].vue new file mode 100644 index 00000000000..81430e4734e --- /dev/null +++ b/integration/templates/nuxt-node/app/pages/sign-up/[...slug].vue @@ -0,0 +1,3 @@ + diff --git a/integration/templates/nuxt-node/app/pages/user.vue b/integration/templates/nuxt-node/app/pages/user-profile/[...slug].vue similarity index 100% rename from integration/templates/nuxt-node/app/pages/user.vue rename to integration/templates/nuxt-node/app/pages/user-profile/[...slug].vue diff --git a/integration/templates/nuxt-node/nuxt.config.js b/integration/templates/nuxt-node/nuxt.config.js index f60e469817f..68df62e2b2b 100644 --- a/integration/templates/nuxt-node/nuxt.config.js +++ b/integration/templates/nuxt-node/nuxt.config.js @@ -1,5 +1,12 @@ export default defineNuxtConfig({ modules: ['@clerk/nuxt'], + clerk: { + appearance: { + options: { + showOptionalFields: true, + }, + }, + }, devtools: { enabled: false, }, diff --git a/integration/templates/react-cra/package.json b/integration/templates/react-cra/package.json index ddf6492bb4f..ebcfd8289fe 100644 --- a/integration/templates/react-cra/package.json +++ b/integration/templates/react-cra/package.json @@ -39,6 +39,6 @@ "@types/react-dom": "18.3.1" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" } } diff --git a/integration/templates/react-cra/src/App.tsx b/integration/templates/react-cra/src/App.tsx index 7689a4c38ed..38197953f08 100644 --- a/integration/templates/react-cra/src/App.tsx +++ b/integration/templates/react-cra/src/App.tsx @@ -1,7 +1,7 @@ // @ts-ignore import React from 'react'; import './App.css'; -import { SignedIn, SignedOut, SignIn, UserButton } from '@clerk/clerk-react'; +import { SignedIn, SignedOut, SignIn, UserButton } from '@clerk/react'; function App() { return ( @@ -10,7 +10,7 @@ function App() {
Signed In - + ); } diff --git a/integration/templates/react-cra/src/index.tsx b/integration/templates/react-cra/src/index.tsx index 3f52fdb0a98..65271a25b7e 100644 --- a/integration/templates/react-cra/src/index.tsx +++ b/integration/templates/react-cra/src/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; -import { ClerkProvider } from '@clerk/clerk-react'; +import { ClerkProvider } from '@clerk/react'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( @@ -10,6 +10,12 @@ root.render( diff --git a/integration/templates/react-router-library/src/main.tsx b/integration/templates/react-router-library/src/main.tsx index 46ab36679fd..82c2ee8e68d 100644 --- a/integration/templates/react-router-library/src/main.tsx +++ b/integration/templates/react-router-library/src/main.tsx @@ -6,11 +6,22 @@ import './index.css'; import App from './App.tsx'; const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; +const CLERK_JS_URL = import.meta.env.VITE_CLERK_JS_URL; +const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL; createRoot(document.getElementById('root')!).render( - + +
diff --git a/integration/templates/react-vite/package.json b/integration/templates/react-vite/package.json index 49105904108..97ace6085d8 100644 --- a/integration/templates/react-vite/package.json +++ b/integration/templates/react-vite/package.json @@ -28,6 +28,6 @@ "vite": "^4.3.9" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" } } diff --git a/integration/templates/react-vite/src/App.tsx b/integration/templates/react-vite/src/App.tsx index 57996dd8890..3c7aabd5906 100644 --- a/integration/templates/react-vite/src/App.tsx +++ b/integration/templates/react-vite/src/App.tsx @@ -1,4 +1,4 @@ -import { OrganizationSwitcher, SignedIn, SignedOut, UserButton } from '@clerk/clerk-react'; +import { OrganizationSwitcher, SignedIn, SignedOut, UserButton } from '@clerk/react'; import { Link } from 'react-router-dom'; import React from 'react'; import { ClientId } from './client-id'; @@ -6,7 +6,7 @@ import { ClientId } from './client-id'; function App() { return (
- + Loading organization switcher} /> SignedOut diff --git a/integration/templates/react-vite/src/buttons/index.tsx b/integration/templates/react-vite/src/buttons/index.tsx index 5aa32d433cf..4b5510477b3 100644 --- a/integration/templates/react-vite/src/buttons/index.tsx +++ b/integration/templates/react-vite/src/buttons/index.tsx @@ -1,4 +1,4 @@ -import { SignInButton, SignUpButton } from '@clerk/clerk-react'; +import { SignInButton, SignUpButton } from '@clerk/react'; export default function Home() { return ( diff --git a/integration/templates/react-vite/src/clerk-status/index.tsx b/integration/templates/react-vite/src/clerk-status/index.tsx index eaae6b01baf..6c5c1e0c5f0 100644 --- a/integration/templates/react-vite/src/clerk-status/index.tsx +++ b/integration/templates/react-vite/src/clerk-status/index.tsx @@ -1,4 +1,4 @@ -import { ClerkLoaded, ClerkLoading, ClerkFailed, ClerkDegraded, useClerk } from '@clerk/clerk-react'; +import { ClerkLoaded, ClerkLoading, ClerkFailed, ClerkDegraded, useClerk } from '@clerk/react'; export default function ClerkStatusPage() { const { loaded, status } = useClerk(); diff --git a/integration/templates/react-vite/src/client-id.tsx b/integration/templates/react-vite/src/client-id.tsx index 88ccc8cf7cc..84f907c1d26 100644 --- a/integration/templates/react-vite/src/client-id.tsx +++ b/integration/templates/react-vite/src/client-id.tsx @@ -1,4 +1,4 @@ -import { useClerk, useSession } from '@clerk/clerk-react'; +import { useClerk, useSession } from '@clerk/react'; export function ClientId() { const clerk = useClerk(); diff --git a/integration/templates/react-vite/src/create-organization/index.tsx b/integration/templates/react-vite/src/create-organization/index.tsx index 7f268110e72..466529f98eb 100644 --- a/integration/templates/react-vite/src/create-organization/index.tsx +++ b/integration/templates/react-vite/src/create-organization/index.tsx @@ -1,4 +1,4 @@ -import { CreateOrganization } from '@clerk/clerk-react'; +import { CreateOrganization } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/custom-user-button-trigger/index.tsx b/integration/templates/react-vite/src/custom-user-button-trigger/index.tsx index bbcd41b52e9..b387a7f36fc 100644 --- a/integration/templates/react-vite/src/custom-user-button-trigger/index.tsx +++ b/integration/templates/react-vite/src/custom-user-button-trigger/index.tsx @@ -1,4 +1,4 @@ -import { UserButton } from '@clerk/clerk-react'; +import { UserButton } from '@clerk/react'; import { PropsWithChildren, useContext, useState } from 'react'; import { PageContext, PageContextProvider } from '../PageContext.tsx'; diff --git a/integration/templates/react-vite/src/custom-user-button/index.tsx b/integration/templates/react-vite/src/custom-user-button/index.tsx index 728bb51f439..77a7199781a 100644 --- a/integration/templates/react-vite/src/custom-user-button/index.tsx +++ b/integration/templates/react-vite/src/custom-user-button/index.tsx @@ -1,4 +1,4 @@ -import { UserButton } from '@clerk/clerk-react'; +import { UserButton } from '@clerk/react'; import { useContext } from 'react'; import { PageContext, PageContextProvider } from '../PageContext.tsx'; diff --git a/integration/templates/react-vite/src/custom-user-button/with-dynamic-items.tsx b/integration/templates/react-vite/src/custom-user-button/with-dynamic-items.tsx index 5295b353e84..167dba77fb7 100644 --- a/integration/templates/react-vite/src/custom-user-button/with-dynamic-items.tsx +++ b/integration/templates/react-vite/src/custom-user-button/with-dynamic-items.tsx @@ -1,4 +1,4 @@ -import { UserButton } from '@clerk/clerk-react'; +import { UserButton } from '@clerk/react'; import { PageContextProvider } from '../PageContext.tsx'; import { useState } from 'react'; diff --git a/integration/templates/react-vite/src/custom-user-button/with-dynamic-label-and-custom-pages.tsx b/integration/templates/react-vite/src/custom-user-button/with-dynamic-label-and-custom-pages.tsx index 45527039c27..3cae5135ace 100644 --- a/integration/templates/react-vite/src/custom-user-button/with-dynamic-label-and-custom-pages.tsx +++ b/integration/templates/react-vite/src/custom-user-button/with-dynamic-label-and-custom-pages.tsx @@ -1,4 +1,4 @@ -import { UserButton } from '@clerk/clerk-react'; +import { UserButton } from '@clerk/react'; import { PageContextProvider } from '../PageContext.tsx'; import React from 'react'; diff --git a/integration/templates/react-vite/src/custom-user-button/with-dynamic-labels.tsx b/integration/templates/react-vite/src/custom-user-button/with-dynamic-labels.tsx index 8ad9aab0dae..6d80cc2dee5 100644 --- a/integration/templates/react-vite/src/custom-user-button/with-dynamic-labels.tsx +++ b/integration/templates/react-vite/src/custom-user-button/with-dynamic-labels.tsx @@ -1,4 +1,4 @@ -import { UserButton } from '@clerk/clerk-react'; +import { UserButton } from '@clerk/react'; import { PageContextProvider } from '../PageContext.tsx'; import React from 'react'; diff --git a/integration/templates/react-vite/src/custom-user-profile/index.tsx b/integration/templates/react-vite/src/custom-user-profile/index.tsx index c6f2fa42e8d..f44e25f0e90 100644 --- a/integration/templates/react-vite/src/custom-user-profile/index.tsx +++ b/integration/templates/react-vite/src/custom-user-profile/index.tsx @@ -1,4 +1,4 @@ -import { UserProfile } from '@clerk/clerk-react'; +import { UserProfile } from '@clerk/react'; import { useContext } from 'react'; import { PageContext, PageContextProvider } from '../PageContext.tsx'; diff --git a/integration/templates/react-vite/src/main.tsx b/integration/templates/react-vite/src/main.tsx index b337553375d..7f60469deeb 100644 --- a/integration/templates/react-vite/src/main.tsx +++ b/integration/templates/react-vite/src/main.tsx @@ -1,4 +1,4 @@ -import { ClerkProvider } from '@clerk/clerk-react'; +import { ClerkProvider } from '@clerk/react'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { createBrowserRouter, Outlet, RouterProvider, useNavigate } from 'react-router-dom'; @@ -31,8 +31,14 @@ const Root = () => { // @ts-ignore publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string} clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL as string} + clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL as string} routerPush={(to: string) => navigate(to)} routerReplace={(to: string) => navigate(to, { replace: true })} + appearance={{ + options: { + showOptionalFields: true, + }, + }} experimental={{ persistClient: import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT ? import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT === 'true' diff --git a/integration/templates/react-vite/src/organization-list/index.tsx b/integration/templates/react-vite/src/organization-list/index.tsx index 393856f058a..d35a442a03d 100644 --- a/integration/templates/react-vite/src/organization-list/index.tsx +++ b/integration/templates/react-vite/src/organization-list/index.tsx @@ -1,4 +1,4 @@ -import { OrganizationList } from '@clerk/clerk-react'; +import { OrganizationList } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/organization-profile/index.tsx b/integration/templates/react-vite/src/organization-profile/index.tsx index 144b8b1a537..183d8a6bd7e 100644 --- a/integration/templates/react-vite/src/organization-profile/index.tsx +++ b/integration/templates/react-vite/src/organization-profile/index.tsx @@ -1,4 +1,4 @@ -import { OrganizationProfile } from '@clerk/clerk-react'; +import { OrganizationProfile } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/organization-switcher/index.tsx b/integration/templates/react-vite/src/organization-switcher/index.tsx index cce7878d001..c929ea46de9 100644 --- a/integration/templates/react-vite/src/organization-switcher/index.tsx +++ b/integration/templates/react-vite/src/organization-switcher/index.tsx @@ -1,4 +1,4 @@ -import { OrganizationSwitcher } from '@clerk/clerk-react'; +import { OrganizationSwitcher } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/protected/index.tsx b/integration/templates/react-vite/src/protected/index.tsx index 3130475df2a..2eb58aa8d76 100644 --- a/integration/templates/react-vite/src/protected/index.tsx +++ b/integration/templates/react-vite/src/protected/index.tsx @@ -1,4 +1,4 @@ -import { SignedIn } from '@clerk/clerk-react'; +import { SignedIn } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/sign-in/index.tsx b/integration/templates/react-vite/src/sign-in/index.tsx index 7ec25930367..b1e0d12a2e3 100644 --- a/integration/templates/react-vite/src/sign-in/index.tsx +++ b/integration/templates/react-vite/src/sign-in/index.tsx @@ -1,4 +1,4 @@ -import { SignIn } from '@clerk/clerk-react'; +import { SignIn } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/sign-up/index.tsx b/integration/templates/react-vite/src/sign-up/index.tsx index fa00b90a68a..1aa91997e56 100644 --- a/integration/templates/react-vite/src/sign-up/index.tsx +++ b/integration/templates/react-vite/src/sign-up/index.tsx @@ -1,4 +1,4 @@ -import { SignUp } from '@clerk/clerk-react'; +import { SignUp } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/user-avatar/index.tsx b/integration/templates/react-vite/src/user-avatar/index.tsx index d608db004a8..dbbbad32fa0 100644 --- a/integration/templates/react-vite/src/user-avatar/index.tsx +++ b/integration/templates/react-vite/src/user-avatar/index.tsx @@ -1,4 +1,4 @@ -import { UserAvatar } from '@clerk/clerk-react'; +import { UserAvatar } from '@clerk/react'; import React from 'react'; export default function UserAvatarPage() { diff --git a/integration/templates/react-vite/src/user-button/index.tsx b/integration/templates/react-vite/src/user-button/index.tsx index a8c6df3a105..1d17595c78e 100644 --- a/integration/templates/react-vite/src/user-button/index.tsx +++ b/integration/templates/react-vite/src/user-button/index.tsx @@ -1,4 +1,4 @@ -import { UserButton } from '@clerk/clerk-react'; +import { UserButton } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/user/index.tsx b/integration/templates/react-vite/src/user/index.tsx index ca6b2c770f4..d39e4a07d2c 100644 --- a/integration/templates/react-vite/src/user/index.tsx +++ b/integration/templates/react-vite/src/user/index.tsx @@ -1,4 +1,4 @@ -import { UserProfile } from '@clerk/clerk-react'; +import { UserProfile } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/react-vite/src/waitlist/index.tsx b/integration/templates/react-vite/src/waitlist/index.tsx index effbf8a5a49..53b82d06d4b 100644 --- a/integration/templates/react-vite/src/waitlist/index.tsx +++ b/integration/templates/react-vite/src/waitlist/index.tsx @@ -1,4 +1,4 @@ -import { Waitlist } from '@clerk/clerk-react'; +import { Waitlist } from '@clerk/react'; export default function Page() { return ( diff --git a/integration/templates/tanstack-react-start/README.md b/integration/templates/tanstack-react-start/README.md index e76cb6abd8a..ede31f2551c 100644 --- a/integration/templates/tanstack-react-start/README.md +++ b/integration/templates/tanstack-react-start/README.md @@ -11,8 +11,8 @@

Clerk and TanStack Start Quickstart

- - Downloads + + Downloads Discord diff --git a/integration/templates/tanstack-react-start/src/routes/__root.tsx b/integration/templates/tanstack-react-start/src/routes/__root.tsx index ecf10d8fcc2..67915716e14 100644 --- a/integration/templates/tanstack-react-start/src/routes/__root.tsx +++ b/integration/templates/tanstack-react-start/src/routes/__root.tsx @@ -22,7 +22,15 @@ function RootComponent() { function RootDocument({ children }: { children: React.ReactNode }) { return ( - + diff --git a/integration/templates/vue-vite/src/main.ts b/integration/templates/vue-vite/src/main.ts index 375584714b4..f3c1a9e4638 100644 --- a/integration/templates/vue-vite/src/main.ts +++ b/integration/templates/vue-vite/src/main.ts @@ -8,7 +8,13 @@ const app = createApp(App); app.use(clerkPlugin, { publishableKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY, clerkJSUrl: import.meta.env.VITE_CLERK_JS_URL, + clerkUiUrl: import.meta.env.VITE_CLERK_UI_URL, clerkJSVersion: import.meta.env.VITE_CLERK_JS_VERSION, + appearance: { + options: { + showOptionalFields: true, + }, + }, }); app.use(router); app.mount('#app'); diff --git a/integration/testUtils/emailService.ts b/integration/testUtils/emailService.ts index 0de10de747b..c1cb085494d 100644 --- a/integration/testUtils/emailService.ts +++ b/integration/testUtils/emailService.ts @@ -12,8 +12,6 @@ export const createEmailService = () => { const fetcher = async (url: string | URL, init?: RequestInit) => { const headers = new Headers(init?.headers || {}); - // eslint-disable-next-line turbo/no-undeclared-env-vars - headers.set('Mailsac-Key', process.env.MAILSAC_API_KEY); return fetch(url, { ...init, headers }); }; diff --git a/integration/testUtils/usersService.ts b/integration/testUtils/usersService.ts index 52814990c92..eecab30dbf4 100644 --- a/integration/testUtils/usersService.ts +++ b/integration/testUtils/usersService.ts @@ -3,6 +3,19 @@ import { faker } from '@faker-js/faker'; import { hash } from '../models/helpers'; +async function withErrorLogging(operation: string, fn: () => Promise): Promise { + try { + return await fn(); + } catch (e: any) { + console.error(`[usersService] ${operation} failed:`); + console.error(' Status:', e.status); + console.error(' Message:', e.message); + console.error(' Errors:', JSON.stringify(e.errors, null, 2)); + console.error(' Clerk Trace ID:', e.clerkTraceId); + throw e; + } +} + type FakeUserOptions = { /** * A fictional email is an email that contains +clerk_test and can always be verified using 424242 as the OTP. No email will be sent. @@ -126,15 +139,17 @@ export const createUserService = (clerkClient: ClerkClient) => { }; }, createBapiUser: async fakeUser => { - return await clerkClient.users.createUser({ - emailAddress: fakeUser.email !== undefined ? [fakeUser.email] : undefined, - password: fakeUser.password, - firstName: fakeUser.firstName, - lastName: fakeUser.lastName, - phoneNumber: fakeUser.phoneNumber !== undefined ? [fakeUser.phoneNumber] : undefined, - username: fakeUser.username, - skipPasswordRequirement: fakeUser.password === undefined, - }); + return withErrorLogging('createBapiUser', () => + clerkClient.users.createUser({ + emailAddress: fakeUser.email !== undefined ? [fakeUser.email] : undefined, + password: fakeUser.password, + firstName: fakeUser.firstName, + lastName: fakeUser.lastName, + phoneNumber: fakeUser.phoneNumber !== undefined ? [fakeUser.phoneNumber] : undefined, + username: fakeUser.username, + skipPasswordRequirement: fakeUser.password === undefined, + }), + ); }, getOrCreateUser: async fakeUser => { const existingUser = await self.getUser({ email: fakeUser.email }); @@ -147,10 +162,12 @@ export const createUserService = (clerkClient: ClerkClient) => { let id = opts.id; if (!id) { - const { data: users } = await clerkClient.users.getUserList({ - emailAddress: [opts.email], - phoneNumber: [opts.phoneNumber], - }); + const { data: users } = await withErrorLogging('getUserList', () => + clerkClient.users.getUserList({ + emailAddress: [opts.email], + phoneNumber: [opts.phoneNumber], + }), + ); id = users[0]?.id; } @@ -159,12 +176,12 @@ export const createUserService = (clerkClient: ClerkClient) => { return; } - await clerkClient.users.deleteUser(id); + await withErrorLogging('deleteUser', () => clerkClient.users.deleteUser(id)); }, getUser: async (opts: { id?: string; email?: string }) => { if (opts.id) { try { - const user = await clerkClient.users.getUser(opts.id); + const user = await withErrorLogging('getUser', () => clerkClient.users.getUser(opts.id)); return user; } catch (err) { console.log(`Error fetching user "${opts.id}": ${err.message}`); @@ -173,7 +190,9 @@ export const createUserService = (clerkClient: ClerkClient) => { } if (opts.email) { - const { data: users } = await clerkClient.users.getUserList({ emailAddress: [opts.email] }); + const { data: users } = await withErrorLogging('getUserList', () => + clerkClient.users.getUserList({ emailAddress: [opts.email] }), + ); if (users.length > 0) { return users[0]; } else { @@ -186,33 +205,41 @@ export const createUserService = (clerkClient: ClerkClient) => { }, createFakeOrganization: async userId => { const name = faker.animal.dog(); - const organization = await clerkClient.organizations.createOrganization({ - name: faker.animal.dog(), - createdBy: userId, - }); + const organization = await withErrorLogging('createOrganization', () => + clerkClient.organizations.createOrganization({ + name: faker.animal.dog(), + createdBy: userId, + }), + ); return { name, organization, - delete: () => clerkClient.organizations.deleteOrganization(organization.id), + delete: () => + withErrorLogging('deleteOrganization', () => clerkClient.organizations.deleteOrganization(organization.id)), } satisfies FakeOrganization; }, createFakeAPIKey: async (userId: string) => { const TWENTY_MINUTES = 20 * 60; - const apiKey = await clerkClient.apiKeys.create({ - subject: userId, - name: `Integration Test - ${faker.string.uuid()}`, - secondsUntilExpiration: TWENTY_MINUTES, - }); + const apiKey = await withErrorLogging('createAPIKey', () => + clerkClient.apiKeys.create({ + subject: userId, + name: `Integration Test - ${faker.string.uuid()}`, + secondsUntilExpiration: TWENTY_MINUTES, + }), + ); return { apiKey, secret: apiKey.secret ?? '', - revoke: () => clerkClient.apiKeys.revoke({ apiKeyId: apiKey.id, revocationReason: 'For testing purposes' }), + revoke: () => + withErrorLogging('revokeAPIKey', () => + clerkClient.apiKeys.revoke({ apiKeyId: apiKey.id, revocationReason: 'For testing purposes' }), + ), } satisfies FakeAPIKey; }, passwordCompromised: async (userId: string) => { - await clerkClient.users.__experimental_passwordCompromised(userId); + await withErrorLogging('passwordCompromised', () => clerkClient.users.__experimental_passwordCompromised(userId)); }, }; diff --git a/integration/tests/appearance.test.ts b/integration/tests/appearance.test.ts index 589d6eff679..4071784fb59 100644 --- a/integration/tests/appearance.test.ts +++ b/integration/tests/appearance.test.ts @@ -14,8 +14,8 @@ test.describe('appearance prop', () => { .addFile( 'src/App.tsx', ({ tsx }) => tsx` - import { SignIn, SignUp } from '@clerk/clerk-react'; - import { dark, neobrutalism, shadesOfPurple } from '@clerk/themes'; + import { SignIn, SignUp } from '@clerk/react'; + import { dark, neobrutalism, shadesOfPurple } from '@clerk/ui/themes'; const themes = { shadesOfPurple, neobrutalism, dark }; export default function App() { @@ -26,8 +26,8 @@ test.describe('appearance prop', () => { return (

{name}

- - + +
); }); diff --git a/integration/tests/billing-hooks.test.ts b/integration/tests/billing-hooks.test.ts index 48c441e6104..daa474f52a4 100644 --- a/integration/tests/billing-hooks.test.ts +++ b/integration/tests/billing-hooks.test.ts @@ -1,10 +1,9 @@ import { expect, test } from '@playwright/test'; -import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('billing hooks @billing', ({ app }) => { +testAgainstRunningApps({})('billing hooks @billing', ({ app }) => { test.describe.configure({ mode: 'parallel' }); test.skip(!app.name.includes('next'), 'Skipping: Only runs on next'); diff --git a/integration/tests/custom-flows/sign-in.test.ts b/integration/tests/custom-flows/sign-in.test.ts index c8fe5851790..30a34e0d190 100644 --- a/integration/tests/custom-flows/sign-in.test.ts +++ b/integration/tests/custom-flows/sign-in.test.ts @@ -1,5 +1,3 @@ -import { parsePublishableKey } from '@clerk/shared/keys'; -import { clerkSetup } from '@clerk/testing/playwright'; import { expect, test } from '@playwright/test'; import type { Application } from '../../models/application'; @@ -19,20 +17,6 @@ test.describe('Custom Flows Sign In @custom', () => { await app.withEnv(appConfigs.envs.withEmailCodes); await app.dev(); - const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); - const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); - const apiUrl = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_API_URL'); - const { frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); - - await clerkSetup({ - publishableKey, - frontendApiUrl, - secretKey, - // @ts-expect-error - apiUrl, - dotenv: false, - }); - const u = createTestUtils({ app }); fakeUser = u.services.users.createFakeUser({ fictionalEmail: true, diff --git a/integration/tests/custom-flows/sign-up.test.ts b/integration/tests/custom-flows/sign-up.test.ts index 02e16f02051..f7c16143755 100644 --- a/integration/tests/custom-flows/sign-up.test.ts +++ b/integration/tests/custom-flows/sign-up.test.ts @@ -1,5 +1,3 @@ -import { parsePublishableKey } from '@clerk/shared/keys'; -import { clerkSetup } from '@clerk/testing/playwright'; import { expect, test } from '@playwright/test'; import type { Application } from '../../models/application'; @@ -19,20 +17,6 @@ test.describe('Custom Flows Sign Up @custom', () => { await app.withEnv(appConfigs.envs.withEmailCodes); await app.dev(); - const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); - const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); - const apiUrl = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_API_URL'); - const { frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); - - await clerkSetup({ - publishableKey, - frontendApiUrl, - secretKey, - // @ts-expect-error - apiUrl, - dotenv: false, - }); - const u = createTestUtils({ app }); fakeUser = u.services.users.createFakeUser({ fictionalEmail: true, diff --git a/integration/tests/elements/next-sign-in.test.ts b/integration/tests/elements/next-sign-in.test.ts deleted file mode 100644 index 6534f28d3e5..00000000000 --- a/integration/tests/elements/next-sign-in.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { appConfigs } from '../../presets'; -import type { FakeUser } from '../../testUtils'; -import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; - -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js Sign-In Flow @elements', ({ app }) => { - test.describe.configure({ mode: 'serial' }); - - let fakeUser: FakeUser; - - test.beforeAll(async () => { - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser({ - fictionalEmail: true, - withPhoneNumber: true, - withUsername: true, - }); - await u.services.users.createBapiUser(fakeUser); - }); - - test.afterAll(async () => { - await fakeUser.deleteIfExists(); - await app.teardown(); - }); - - test.afterEach(async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.page.signOut(); - await u.page.context().clearCookies(); - }); - - test('sign in with email and password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - - await u.po.signIn.setIdentifier(fakeUser.email); - await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/continue'); - await u.po.signIn.setPassword(fakeUser.password); - await u.po.signIn.continue(); - - await u.po.expect.toBeSignedIn(); - }); - - test('sign in with email and instant password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - - await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); - - await u.po.expect.toBeSignedIn(); - }); - - test('does not allow arbitrary redirect URLs on sign in', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.goTo({ - searchParams: new URLSearchParams({ redirect_url: 'https://evil.com' }), - headlessSelector: '[data-test-id="sign-in-step-start"]', - }); - - await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); - - expect(u.page.url()).not.toContain('https://evil.com'); - - await u.po.expect.toBeSignedIn(); - }); - - test('sign in with email code', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - - await u.po.signIn.setIdentifier(fakeUser.email); - await u.po.signIn.continue(); - - await u.page.getByRole('button', { name: /use another method/i }).click(); - await u.po.signIn.getAltMethodsEmailCodeButton().click(); - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signIn.continue(); - - await u.page.waitForAppUrl('/'); - await u.po.expect.toBeSignedIn(); - }); - - test('sign in with phone number and password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - - await u.page.getByRole('button', { name: /^use phone/i }).click(); - await u.po.signIn.getIdentifierInput().fill(fakeUser.phoneNumber); - await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/continue'); - await u.po.signIn.setPassword(fakeUser.password); - await u.po.signIn.continue(); - - await u.po.expect.toBeSignedIn(); - }); - - test('sign in only with phone number', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUserWithoutPassword = u.services.users.createFakeUser({ - fictionalEmail: true, - withPassword: false, - withPhoneNumber: true, - }); - await u.services.users.createBapiUser(fakeUserWithoutPassword); - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - await u.page.getByRole('button', { name: /^use phone/i }).click(); - await u.po.signIn.getIdentifierInput().fill(fakeUserWithoutPassword.phoneNumber); - await u.po.signIn.continue(); - await page.getByRole('textbox', { name: 'Enter phone verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signIn.continue(); - - await u.po.expect.toBeSignedIn(); - - await fakeUserWithoutPassword.deleteIfExists(); - }); - - test('sign in with username and password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - - await u.po.signIn.getIdentifierInput().fill(fakeUser.username); - await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/continue'); - await u.po.signIn.setPassword(fakeUser.password); - await u.po.signIn.continue(); - - await u.po.expect.toBeSignedIn(); - }); - - test('can reset password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUserWithPasword = u.services.users.createFakeUser({ - fictionalEmail: true, - withPassword: true, - }); - await u.services.users.createBapiUser(fakeUserWithPasword); - - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - - await u.po.signIn.getIdentifierInput().fill(fakeUserWithPasword.email); - await u.po.signIn.continue(); - await u.page.getByRole('button', { name: /^forgot password/i }).click(); - await u.po.signIn.getResetPassword().click(); - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signIn.continue(); - - await u.po.signIn.setPassword(`${fakeUserWithPasword.password}_reset`); - await u.po.signIn.setPasswordConfirmation(`${fakeUserWithPasword.password}_reset`); - await u.po.signIn.getResetPassword().click(); - await u.po.expect.toBeSignedIn(); - - await fakeUserWithPasword.deleteIfExists(); - }); - - test('cannot sign in with wrong password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - await u.po.signIn.getIdentifierInput().fill(fakeUser.email); - await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/continue'); - await u.po.signIn.setPassword('wrong-password'); - await u.po.signIn.continue(); - await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible(); - - await u.po.expect.toBeSignedOut(); - }); - - test('cannot sign in with wrong password but can sign in with email', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - await u.po.signIn.getIdentifierInput().fill(fakeUser.email); - await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/continue'); - await u.po.signIn.setPassword('wrong-password'); - await u.po.signIn.continue(); - - await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible(); - - await u.page.getByRole('button', { name: /use another method/i }).click(); - await u.po.signIn.getAltMethodsEmailCodeButton().click(); - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signIn.continue(); - - await u.po.expect.toBeSignedIn(); - }); -}); diff --git a/integration/tests/elements/next-sign-up.test.ts b/integration/tests/elements/next-sign-up.test.ts deleted file mode 100644 index 70f7d42dd25..00000000000 --- a/integration/tests/elements/next-sign-up.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { appConfigs } from '../../presets'; -import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; - -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Next.js Sign-Up Flow @elements', ({ app }) => { - test.describe.configure({ mode: 'serial' }); - - test.afterAll(async () => { - await app.teardown(); - }); - - test('sign up with email and password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUser = u.services.users.createFakeUser({ - fictionalEmail: true, - withPhoneNumber: true, - withUsername: true, - }); - - await u.po.signUp.goTo({ headlessSelector: '[data-test-id="sign-up-step-start"]' }); - - await u.po.signUp.signUpWithEmailAndPassword({ - email: fakeUser.email, - password: fakeUser.password, - }); - - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await u.page.waitForAppUrl('/'); - await u.po.expect.toBeSignedIn(); - - await fakeUser.deleteIfExists(); - }); - - test('does not allow arbitrary redirect URLs on sign up', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUser = u.services.users.createFakeUser({ - fictionalEmail: true, - withPhoneNumber: true, - withUsername: true, - }); - - await u.po.signUp.goTo({ - searchParams: new URLSearchParams({ redirect_url: 'https://evil.com' }), - headlessSelector: '[data-test-id="sign-up-step-start"]', - }); - - await u.po.signUp.signUpWithEmailAndPassword({ - email: fakeUser.email, - password: fakeUser.password, - }); - - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await u.page.waitForAppUrl('/'); - await u.po.expect.toBeSignedIn(); - - await fakeUser.deleteIfExists(); - }); - - test("can't sign up with weak password", async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUser = u.services.users.createFakeUser({ - fictionalEmail: true, - withPhoneNumber: true, - withUsername: true, - }); - - await u.po.signUp.goTo({ headlessSelector: '[data-test-id="sign-up-step-start"]' }); - - await u.po.signUp.signUpWithEmailAndPassword({ - email: fakeUser.email, - password: '12345', - }); - - // Check if password error is visible - await expect(u.page.getByText(/Passwords must be \d+ characters or more/i)).toBeVisible(); - - await u.po.expect.toBeSignedOut(); - - await fakeUser.deleteIfExists(); - }); - - test('can sign up with phone number', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUser = u.services.users.createFakeUser({ - fictionalEmail: true, - withPhoneNumber: true, - withUsername: true, - }); - - await u.po.signUp.goTo({ headlessSelector: '[data-test-id="sign-up-step-start"]' }); - - await u.po.signUp.signUp({ - email: fakeUser.email, - phoneNumber: fakeUser.phoneNumber, - password: fakeUser.password, - }); - - await page.getByRole('textbox', { name: 'Enter phone verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await u.po.expect.toBeSignedIn(); - await fakeUser.deleteIfExists(); - }); - - test('sign up with first name, last name, email, phone and password', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUser = u.services.users.createFakeUser({ - fictionalEmail: true, - withPhoneNumber: true, - withUsername: true, - }); - - await u.po.signUp.goTo({ headlessSelector: '[data-test-id="sign-up-step-start"]' }); - - await u.po.signUp.signUp({ - username: fakeUser.username, - email: fakeUser.email, - phoneNumber: fakeUser.phoneNumber, - password: fakeUser.password, - }); - - await page.getByRole('textbox', { name: 'Enter phone verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await u.po.expect.toBeSignedIn(); - - await fakeUser.deleteIfExists(); - }); - - test('sign up, sign out and sign in again', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - const fakeUser = u.services.users.createFakeUser({ - fictionalEmail: true, - withPhoneNumber: true, - withUsername: true, - }); - - await u.po.signUp.goTo({ headlessSelector: '[data-test-id="sign-up-step-start"]' }); - - await u.po.signUp.signUp({ - username: fakeUser.username, - email: fakeUser.email, - phoneNumber: fakeUser.phoneNumber, - password: fakeUser.password, - }); - - await page.getByRole('textbox', { name: 'Enter phone verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await page.getByRole('textbox', { name: 'Enter email verification code' }).click(); - await page.keyboard.type('424242', { delay: 100 }); - await u.po.signUp.continue(); - - await u.po.expect.toBeSignedIn(); - - await u.page.evaluate(async () => { - await window.Clerk.signOut(); - }); - - await u.po.expect.toBeSignedOut(); - - await u.po.signIn.goTo({ headlessSelector: '[data-test-id="sign-in-step-start"]' }); - - await u.po.signIn.setIdentifier(fakeUser.email); - await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/continue'); - await u.po.signIn.setPassword(fakeUser.password); - await u.po.signIn.continue(); - - await u.po.expect.toBeSignedIn(); - - await fakeUser.deleteIfExists(); - }); -}); diff --git a/integration/tests/elements/otp.test.ts b/integration/tests/elements/otp.test.ts deleted file mode 100644 index 59f63f3414f..00000000000 --- a/integration/tests/elements/otp.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { appConfigs } from '../../presets'; -import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; - -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('OTP @elements', ({ app }) => { - test.describe.configure({ mode: 'parallel' }); - - test.afterAll(async () => { - await app.teardown(); - }); - - test.beforeEach(async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.page.goToRelative('/otp'); - }); - - const otpTypes = { - simpleOtp: 'simple-otp', - segmentedOtp: 'segmented-otp', - segmentedOtpWithProps: 'segmented-otp-with-props', - } as const; - - for (const otpType of [otpTypes.simpleOtp, otpTypes.segmentedOtp]) { - test.describe(`Type: ${otpType}`, () => { - test(`should receive correct standard props`, async ({ page }) => { - const otp = page.getByTestId(otpType); - - await expect(otp).toHaveAttribute('autocomplete', 'one-time-code'); - await expect(otp).toHaveAttribute('spellcheck', 'false'); - await expect(otp).toHaveAttribute('inputmode', 'numeric'); - await expect(otp).toHaveAttribute('maxlength', '6'); - await expect(otp).toHaveAttribute('minlength', '6'); - await expect(otp).toHaveAttribute('pattern', '[0-9]{6}'); - await expect(otp).toHaveAttribute('type', 'text'); - }); - - test(`should change the input value`, async ({ page }) => { - const otp = page.getByTestId(otpType); - - // Check that the input starts with an empty value - await expect(otp).toHaveValue(''); - - await otp.pressSequentially('1'); - await expect(otp).toHaveValue('1'); - - await otp.pressSequentially('23456'); - await expect(otp).toHaveValue('123456'); - }); - }); - } - - test.describe(`Type: ${otpTypes.simpleOtp}`, () => { - test(`should prevent typing greater than max length`, async ({ page }) => { - const otp = page.getByTestId(otpTypes.simpleOtp); - - await otp.pressSequentially('1234567'); - await expect(otp).toHaveValue('123456'); - }); - }); - - test.describe(`Type: ${otpTypes.segmentedOtp}`, () => { - test('renders hidden segments', async ({ page }) => { - const otpSegmentsWrapper = page.locator('.segmented-otp-wrapper'); - - await expect(otpSegmentsWrapper).toHaveAttribute('aria-hidden', 'true'); - // Check that 6 segments are rendered - await expect(otpSegmentsWrapper.locator('> div')).toHaveCount(6); - }); - - test(`should prevent typing greater than max length`, async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('1234567'); - // With the segmented OTP we expect the last char to be replaced by any new input - await expect(otp).toHaveValue('123457'); - }); - - test(`should put values into segments`, async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - // Check initial state before any interaction - for (let i = 0; i < 6; i++) { - await expect(page.getByTestId(`segmented-otp-${i}`)).toHaveText(''); - await expect(page.getByTestId(`segmented-otp-${i}`)).toHaveAttribute('data-status', 'none'); - } - - await otp.pressSequentially('123456'); - - for (let i = 0; i < 6; i++) { - await expect(page.getByTestId(`segmented-otp-${i}`)).toHaveText(`${i + 1}`); - } - }); - - test('should set hover status on segments', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.hover(); - for (let i = 0; i < 6; i++) { - await expect(page.getByTestId(`segmented-otp-${i}`)).toHaveAttribute('data-status', 'hovered'); - } - }); - - test('should not set hover status on segments if they are focused', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('123'); - await otp.hover(); - for (let i = 0; i < 6; i++) { - await expect(page.getByTestId(`segmented-otp-${i}`)).not.toHaveAttribute('data-status', 'hovered'); - } - }); - - test('should set cursor and selected status on segments', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('12'); - - await expect(page.getByTestId('segmented-otp-0')).toHaveAttribute('data-status', 'none'); - await expect(page.getByTestId('segmented-otp-1')).toHaveAttribute('data-status', 'none'); - await expect(page.getByTestId('segmented-otp-2')).toHaveAttribute('data-status', 'cursor'); - - await otp.press('ArrowLeft'); - - await expect(page.getByTestId('segmented-otp-0')).toHaveAttribute('data-status', 'none'); - await expect(page.getByTestId('segmented-otp-1')).toHaveAttribute('data-status', 'selected'); - await expect(page.getByTestId('segmented-otp-2')).toHaveAttribute('data-status', 'none'); - - await otp.press('ArrowLeft'); - - await expect(page.getByTestId('segmented-otp-0')).toHaveAttribute('data-status', 'selected'); - await expect(page.getByTestId('segmented-otp-1')).toHaveAttribute('data-status', 'none'); - }); - - test('should replace selected segment with new input', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('12'); - - await otp.press('ArrowLeft'); - await otp.pressSequentially('1'); - await expect(otp).toHaveValue('11'); - }); - - test('should replace multi-selected segments with new input', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('12345'); - // Mark two segments to the left of the cursor - await otp.press('Shift+ArrowLeft'); - await otp.press('Shift+ArrowLeft'); - await expect(page.getByTestId('segmented-otp-3')).toHaveAttribute('data-status', 'selected'); - await expect(page.getByTestId('segmented-otp-4')).toHaveAttribute('data-status', 'selected'); - await otp.pressSequentially('1'); - - await expect(otp).toHaveValue('1231'); - - // Mark all segments - await otp.press('ControlOrMeta+a'); - await otp.pressSequentially('1'); - - await expect(otp).toHaveValue('1'); - }); - - test('should backspace char', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('123'); - await otp.press('Backspace'); - - await expect(otp).toHaveValue('12'); - await expect(page.getByTestId('segmented-otp-2')).toHaveAttribute('data-status', 'cursor'); - }); - - test('should backspace all chars with modifier', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('123'); - await otp.press('ControlOrMeta+Backspace'); - - await expect(otp).toHaveValue(''); - await expect(page.getByTestId('segmented-otp-0')).toHaveAttribute('data-status', 'cursor'); - }); - - test('should backspace selected char', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('123'); - await otp.press('ArrowLeft'); - await otp.press('ArrowLeft'); - await otp.press('Backspace'); - - await expect(otp).toHaveValue('13'); - }); - - test('should forward-delete char when pressing delete', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtp); - - await otp.pressSequentially('1234'); - - await otp.press('ArrowLeft'); - await otp.press('ArrowLeft'); - await otp.press('Delete'); - - await expect(otp).toHaveValue('124'); - await otp.press('ArrowRight'); - await otp.press('Delete'); - await expect(otp).toHaveValue('12'); - }); - }); - - test.describe('Custom props', () => { - test('length', async ({ page }) => { - const otp = page.getByTestId(otpTypes.segmentedOtpWithProps); - const otpSegmentsWrapper = page.locator('.segmented-otp-with-props-wrapper'); - - await expect(otp).toHaveAttribute('maxlength', '4'); - await expect(otp).toHaveAttribute('minlength', '4'); - await expect(otp).toHaveAttribute('pattern', '[0-9]{4}'); - - // Check that only 4 segments are rendered - await expect(otpSegmentsWrapper.locator('> div')).toHaveCount(4); - }); - }); -}); diff --git a/integration/tests/elements/validate-password.test.ts b/integration/tests/elements/validate-password.test.ts deleted file mode 100644 index a79999d4d64..00000000000 --- a/integration/tests/elements/validate-password.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { appConfigs } from '../../presets'; -import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; - -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Password Validation @elements', ({ app }) => { - test.describe.configure({ mode: 'parallel' }); - - test.afterAll(async () => { - await app.teardown(); - }); - - test.beforeEach(async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.page.goToRelative('/validate-password'); - }); - - test('should have initial "idle" state', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - await expect(u.po.signIn.getPasswordInput()).toHaveAttribute('data-state', 'idle'); - await expect(page.getByTestId('state')).toHaveText('idle'); - }); - - test('should change state to "info" on focus', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.getPasswordInput().focus(); - - await expect(page.getByTestId('state')).toHaveText('info'); - }); - - test('should return codes and message with non-idle state', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.getPasswordInput().focus(); - - await expect(page.getByTestId('codes')).toHaveText(/min_length/); - await expect(page.getByTestId('message')).toHaveText('Your password must contain 8 or more characters.'); - }); - - test('should return error when requirements are not met', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.setPassword('12345678'); - - await expect(page.getByTestId('state')).toHaveText('error'); - await expect(page.getByTestId('codes')).toHaveText(/require_special_char/); - await expect(page.getByTestId('message')).toHaveText('Your password must contain a special character.'); - }); - - test('should return success when requirements are met', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.po.signIn.setPassword('12345678@'); - - await expect(page.getByTestId('state')).toHaveText('success'); - await expect(page.getByTestId('codes')).toHaveText(''); - await expect(page.getByTestId('message')).toHaveText('Your password meets all the necessary requirements.'); - }); - - test('should have working flow', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - await expect(page.getByTestId('state')).toHaveText('idle'); - await u.po.signIn.setPassword('123'); - await expect(page.getByTestId('state')).toHaveText('info'); - await u.po.signIn.setPassword('12345678'); - await expect(page.getByTestId('state')).toHaveText('error'); - await u.po.signIn.setPassword('12345678@'); - await expect(page.getByTestId('state')).toHaveText('success'); - }); -}); diff --git a/integration/tests/global.setup.ts b/integration/tests/global.setup.ts index 52a373ebe71..377477b4979 100644 --- a/integration/tests/global.setup.ts +++ b/integration/tests/global.setup.ts @@ -2,7 +2,7 @@ import { test as setup } from '@playwright/test'; import { constants } from '../constants'; import { appConfigs } from '../presets'; -import { fs, parseEnvOptions, startClerkJsHttpServer } from '../scripts'; +import { fs, parseEnvOptions, startClerkJsHttpServer, startClerkUiHttpServer } from '../scripts'; setup('start long running apps', async () => { setup.setTimeout(90_000); @@ -10,6 +10,7 @@ setup('start long running apps', async () => { await fs.ensureDir(constants.TMP_DIR); await startClerkJsHttpServer(); + await startClerkUiHttpServer(); const { appIds } = parseEnvOptions(); if (appIds.length) { diff --git a/integration/tests/global.teardown.ts b/integration/tests/global.teardown.ts index e9aee7c0f26..7445ab191c7 100644 --- a/integration/tests/global.teardown.ts +++ b/integration/tests/global.teardown.ts @@ -3,15 +3,16 @@ import { test as setup } from '@playwright/test'; import { constants } from '../constants'; import { stateFile } from '../models/stateFile'; import { appConfigs } from '../presets'; -import { killClerkJsHttpServer, parseEnvOptions } from '../scripts'; +import { killClerkJsHttpServer, killClerkUiHttpServer, parseEnvOptions } from '../scripts'; setup('teardown long running apps', async () => { setup.setTimeout(90_000); const { appUrl } = parseEnvOptions(); await killClerkJsHttpServer(); + await killClerkUiHttpServer(); - if (appUrl || !constants.CLEANUP) { + if (appUrl || !constants.E2E_CLEANUP) { // if appUrl is provided, it means that the user is running an app manually console.log('Skipping cleanup'); return; diff --git a/integration/tests/last-authentication-strategy.test.ts b/integration/tests/last-authentication-strategy.test.ts index 09449c0fde8..194ca1c6a82 100644 --- a/integration/tests/last-authentication-strategy.test.ts +++ b/integration/tests/last-authentication-strategy.test.ts @@ -1,7 +1,7 @@ +import type { LastAuthenticationStrategy } from '@clerk/shared/types'; import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; -import type { LastAuthenticationStrategy } from '../../packages/types'; import { appConfigs } from '../presets'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index b37527e0f36..d9b308cf355 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -1,9 +1,10 @@ import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; +import type { Application } from '../../models/application'; import { appConfigs } from '../../presets'; import type { FakeOrganization, FakeUser } from '../../testUtils'; -import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; +import { createTestUtils } from '../../testUtils'; const mockAPIKeysEnvironmentSettings = async ( page: Page, @@ -27,16 +28,21 @@ const mockAPIKeysEnvironmentSettings = async ( }); }; -testAgainstRunningApps({ - withEnv: [appConfigs.envs.withAPIKeys], - withPattern: ['withMachine.next.appRouter'], -})('api keys component @machine', ({ app }) => { +test.describe('api keys component @machine', () => { test.describe.configure({ mode: 'serial' }); + let app: Application; let fakeAdmin: FakeUser; let fakeOrganization: FakeOrganization; test.beforeAll(async () => { + test.setTimeout(90_000); // Wait for app to be ready + app = await appConfigs.next.appRouter.clone().commit(); + + await app.setup(); + await app.withEnv(appConfigs.envs.withAPIKeys); + await app.dev(); + const u = createTestUtils({ app }); fakeAdmin = u.services.users.createFakeUser(); const admin = await u.services.users.createBapiUser(fakeAdmin); diff --git a/integration/tests/nuxt/basic.test.ts b/integration/tests/nuxt/basic.test.ts index 12bf61f114d..5f48239e37e 100644 --- a/integration/tests/nuxt/basic.test.ts +++ b/integration/tests/nuxt/basic.test.ts @@ -30,21 +30,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await u.page.context().clearCookies(); }); - test('sign in with hash routing', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - - await u.page.goToRelative('/sign-in'); - await u.po.signIn.waitForMounted(); - - await u.po.signIn.setIdentifier(fakeUser.email); - await u.po.signIn.continue(); - await u.page.waitForURL(`${app.serverUrl}/sign-in#/factor-one`); - - await u.po.signIn.setPassword(fakeUser.password); - await u.po.signIn.continue(); - await u.po.expect.toBeSignedIn(); - }); - test('render user profile with SSR data', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); @@ -54,7 +39,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await u.po.expect.toBeSignedIn(); await u.po.userButton.waitForMounted(); - await u.page.goToRelative('/user'); + await u.page.goToRelative('/user-profile'); await u.po.userProfile.waitForMounted(); // Fetched from an API endpoint (/api/me), which is server-rendered. @@ -66,7 +51,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te test('redirects to sign-in when unauthenticated', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); - await u.page.goToRelative('/user'); + await u.page.goToRelative('/user-profile'); await u.page.waitForURL(`${app.serverUrl}/sign-in`); await u.po.signIn.waitForMounted(); }); diff --git a/integration/tests/nuxt/navigation.test.ts b/integration/tests/nuxt/navigation.test.ts new file mode 100644 index 00000000000..de872e9e8e0 --- /dev/null +++ b/integration/tests/nuxt/navigation.test.ts @@ -0,0 +1,76 @@ +import { expect, test } from '@playwright/test'; + +import { appConfigs } from '../../presets'; +import type { FakeUser } from '../../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; + +testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('navigation modes @nuxt', ({ app }) => { + test.describe.configure({ mode: 'serial' }); + let fakeUser: FakeUser; + + test.beforeAll(async () => { + const m = createTestUtils({ app }); + fakeUser = m.services.users.createFakeUser(); + await m.services.users.createBapiUser(fakeUser); + }); + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('sign in with path routing', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.page.waitForURL(`${app.serverUrl}/sign-in/factor-one`); + + await u.po.signIn.setPassword(fakeUser.password); + await u.po.signIn.continue(); + + await u.po.expect.toBeSignedIn(); + }); + + test('sign in with hash routing', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.page.goToRelative('/hash/sign-in'); + await u.po.signIn.waitForMounted(); + + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.page.waitForURL(`${app.serverUrl}/hash/sign-in#/factor-one`); + + await u.po.signIn.setPassword(fakeUser.password); + await u.po.signIn.continue(); + await u.po.expect.toBeSignedIn(); + }); + + test('sign in with path routing navigates to previous page', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + + await u.po.signIn.getGoToSignUp().click(); + await u.po.signUp.waitForMounted(); + await u.page.waitForURL(`${app.serverUrl}/sign-up`); + + await page.goBack(); + await u.po.signIn.waitForMounted(); + await u.page.waitForURL(`${app.serverUrl}/sign-in`); + }); + + test('user profile uses path routing', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/user-profile/security'); + await expect(u.page.locator('.cl-headerTitle').filter({ hasText: 'Security' })).toBeVisible(); + }); +}); diff --git a/integration/tests/pricing-table.test.ts b/integration/tests/pricing-table.test.ts index bdc61418b6c..f54917c6f21 100644 --- a/integration/tests/pricing-table.test.ts +++ b/integration/tests/pricing-table.test.ts @@ -1,11 +1,10 @@ import type { Locator } from '@playwright/test'; import { expect, test } from '@playwright/test'; -import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing table @billing', ({ app }) => { +testAgainstRunningApps({})('pricing table @billing', ({ app }) => { test.describe.configure({ mode: 'parallel' }); let fakeUser: FakeUser; @@ -639,7 +638,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await fakeUser.deleteIfExists(); }); - test('displays notice then plan cannot change', async ({ page, context }) => { + test('displays notice the plan cannot change', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); const fakeUser = u.services.users.createFakeUser(); @@ -657,7 +656,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await u.po.checkout.fillTestCard(); await u.po.checkout.clickPayOrSubscribe(); await expect(u.po.page.getByText('Payment was successful!')).toBeVisible(); - await u.po.checkout.confirmAndContinue(); await u.po.pricingTable.startCheckout({ planSlug: 'pro', shouldSwitch: true, period: 'monthly' }); await u.po.checkout.waitForMounted(); diff --git a/integration/tests/update-props.test.ts b/integration/tests/update-props.test.ts index 597ec0ba8b1..823bd011006 100644 --- a/integration/tests/update-props.test.ts +++ b/integration/tests/update-props.test.ts @@ -31,7 +31,7 @@ testAgainstRunningApps({ withPattern: ['react.vite.withEmailCodes'] })('sign in await u.page.waitForFunction(async () => { // Emulate ClerkProvider being unmounted and mounted again // as updateProps is going to be called without the default options set by window.Clerk.load() - await (window.Clerk as any).__unstable__updateProps({ options: {} }); + await (window.Clerk as any).__internal_updateProps({ options: {} }); }); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); diff --git a/integration/types.d.ts b/integration/types.d.ts index 8df81fba45b..241f1637063 100644 --- a/integration/types.d.ts +++ b/integration/types.d.ts @@ -1,4 +1,4 @@ -import type { Clerk } from '@clerk/types'; +import type { Clerk } from '@clerk/shared/types'; declare global { interface Window { diff --git a/package.json b/package.json index 3d9286c9143..24d2f4af5e7 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,12 @@ "license": "MIT", "scripts": { "build": "FORCE_COLOR=1 turbo build --concurrency=${TURBO_CONCURRENCY:-80%}", - "build:declarations": "FORCE_COLOR=1 turbo build:declarations --concurrency=${TURBO_CONCURRENCY:-80%} --filter=@clerk/nextjs --filter=@clerk/clerk-react --filter=@clerk/shared --filter=@clerk/types", + "build:declarations": "FORCE_COLOR=1 turbo build:declarations --concurrency=${TURBO_CONCURRENCY:-80%} --filter=@clerk/nextjs --filter=@clerk/react --filter=@clerk/shared", "bundlewatch": "turbo bundlewatch", "changeset": "changeset", "changeset:empty": "pnpm changeset --empty", "clean": "turbo clean", - "dev": "TURBO_UI=0 FORCE_COLOR=1 turbo dev --filter=@clerk/* --filter=!@clerk/clerk-expo --filter=!@clerk/tanstack-react-start --filter=!@clerk/elements --filter=!@clerk/remix --filter=!@clerk/chrome-extension", + "dev": "TURBO_UI=0 FORCE_COLOR=1 turbo dev --filter=@clerk/* --filter=!@clerk/expo --filter=!@clerk/tanstack-react-start --filter=!@clerk/chrome-extension", "dev:js": "TURBO_UI=0 FORCE_COLOR=1 turbo dev:current --filter=@clerk/clerk-js", "format": "turbo format && node scripts/format-non-workspace.mjs", "format:check": "turbo format:check && node scripts/format-non-workspace.mjs --check", @@ -26,6 +26,7 @@ "prepare": "husky install", "release": "changeset publish && git push --follow-tags", "release:canary": "changeset publish --tag canary --no-git-tag", + "release:canary-core3": "changeset publish --tag canary-core3 --no-git-tag", "release:snapshot": "changeset publish --tag snapshot --no-git-tag", "release:verdaccio": "if [ \"$(npm config get registry)\" = \"https://registry.npmjs.org/\" ]; then echo 'Error: Using default registry' && exit 1; else TURBO_CONCURRENCY=1 pnpm build && changeset publish --no-git-tag; fi", "test": "FORCE_COLOR=1 turbo test --concurrency=${TURBO_CONCURRENCY:-80%}", @@ -33,18 +34,17 @@ "test:integration:ap-flows": "pnpm test:integration:base --grep @ap-flows", "test:integration:astro": "E2E_APP_ID=astro.* pnpm test:integration:base --grep @astro", "test:integration:base": "pnpm playwright test --config integration/playwright.config.ts", - "test:integration:billing": "E2E_APP_ID=withBilling.* pnpm test:integration:base --grep @billing", + "test:integration:billing": "E2E_APP_ID=withBillingJwtV2.* pnpm test:integration:base --grep @billing", "test:integration:cleanup": "pnpm playwright test --config integration/playwright.cleanup.config.ts", "test:integration:custom": "pnpm test:integration:base --grep @custom", "test:integration:deployment:nextjs": "pnpm playwright test --config integration/playwright.deployments.config.ts", - "test:integration:elements": "E2E_APP_ID=elements.* pnpm test:integration:base --grep @elements", - "test:integration:expo-web": "E2E_APP_ID=expo.expo-web pnpm test:integration:base --grep @expo-web", + "test:integration:expo-web:disabled": "E2E_APP_ID=expo.expo-web pnpm test:integration:base --grep @expo-web", "test:integration:express": "E2E_APP_ID=express.* pnpm test:integration:base --grep @express", "test:integration:generic": "E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* pnpm test:integration:base --grep @generic", "test:integration:handshake": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=sessions-prod-1 E2E_SESSIONS_APP_1_HOST=multiple-apps-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:handshake:staging": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=clerkstage-sessions-prod-1 E2E_SESSIONS_APP_1_HOST=clerkstage-sessions-prod-1-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:localhost": "pnpm test:integration:base --grep @localhost", - "test:integration:machine": "E2E_APP_ID=withMachine.* pnpm test:integration:base --grep @machine", + "test:integration:machine": "pnpm test:integration:base --grep @machine", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.* pnpm test:integration:base --grep @nextjs", "test:integration:nuxt": "E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", "test:integration:quickstart": "E2E_APP_ID=quickstart.* pnpm test:integration:base --grep @quickstart", @@ -55,10 +55,11 @@ "test:integration:vue": "E2E_APP_ID=vue.vite pnpm test:integration:base --grep @vue", "test:typedoc": "pnpm typedoc:generate && cd ./.typedoc && vitest run", "turbo:clean": "turbo daemon clean", - "typedoc:generate": "pnpm build:declarations && pnpm typedoc:generate:skip-build", + "typedoc:generate": "pnpm build && pnpm typedoc:generate:skip-build", "typedoc:generate:skip-build": "typedoc --tsconfig tsconfig.typedoc.json && node .typedoc/extract-returns-and-params.mjs && rimraf .typedoc/docs && cpy '.typedoc/temp-docs/**' '.typedoc/docs' && rimraf .typedoc/temp-docs", "version-packages": "changeset version && pnpm install --lockfile-only --engine-strict=false", "version-packages:canary": "./scripts/canary.mjs", + "version-packages:canary-core3": "./scripts/canary-core3.mjs", "version-packages:snapshot": "./scripts/snapshot.mjs", "yalc:all": "for d in packages/*/; do echo $d; cd $d; pnpm yalc push --replace --sig; cd '../../'; done" }, @@ -154,7 +155,7 @@ }, "packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a", "engines": { - "node": ">=18.17.0", + "node": ">=20.9.0", "pnpm": ">=10.17.1" }, "pnpm": { diff --git a/packages/agent-toolkit/package.json b/packages/agent-toolkit/package.json index 2a5ff9188ec..b4e6d6a4cb1 100644 --- a/packages/agent-toolkit/package.json +++ b/packages/agent-toolkit/package.json @@ -49,7 +49,6 @@ "dependencies": { "@clerk/backend": "workspace:^", "@clerk/shared": "workspace:^", - "@clerk/types": "workspace:^", "@modelcontextprotocol/sdk": "1.7.0", "yargs": "17.7.2", "zod": "3.24.2" diff --git a/packages/astro/package.json b/packages/astro/package.json index 094ad371e7e..093096e962a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -90,18 +90,18 @@ "dependencies": { "@clerk/backend": "workspace:^", "@clerk/shared": "workspace:^", - "@clerk/types": "workspace:^", "nanoid": "5.1.6", "nanostores": "1.0.1" }, "devDependencies": { + "@clerk/ui": "workspace:^", "astro": "^5.15.3" }, "peerDependencies": { "astro": "^4.15.0 || ^5.0.0" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" }, "publishConfig": { "access": "public" diff --git a/packages/astro/src/astro-components/control/AuthenticateWithRedirectCallback.astro b/packages/astro/src/astro-components/control/AuthenticateWithRedirectCallback.astro index aef20144140..43032416254 100644 --- a/packages/astro/src/astro-components/control/AuthenticateWithRedirectCallback.astro +++ b/packages/astro/src/astro-components/control/AuthenticateWithRedirectCallback.astro @@ -1,5 +1,5 @@ --- -import type { HandleOAuthCallbackParams } from '@clerk/types'; +import type { HandleOAuthCallbackParams } from '@clerk/shared/types'; type Props = HandleOAuthCallbackParams; diff --git a/packages/astro/src/astro-components/control/ProtectCSR.astro b/packages/astro/src/astro-components/control/ProtectCSR.astro index cee284935c5..e3aa5ca8f3c 100644 --- a/packages/astro/src/astro-components/control/ProtectCSR.astro +++ b/packages/astro/src/astro-components/control/ProtectCSR.astro @@ -44,7 +44,7 @@ const { + \n`; } diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 6841e1c40df..b40c656de99 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -18,8 +18,8 @@ import { import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler'; import { isHttpOrHttps } from '@clerk/shared/proxy'; +import type { PendingSessionOptions } from '@clerk/shared/types'; import { handleValueOrFn } from '@clerk/shared/utils'; -import type { PendingSessionOptions } from '@clerk/types'; import type { APIContext } from 'astro'; import { authAsyncStorage } from '#async-local-storage'; diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts index c1b21574f8d..7ec1824029b 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -31,6 +31,7 @@ function getSafeEnv(context: ContextOrLocals) { signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context), signUpUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_UP_URL', context), clerkJsUrl: getContextEnvVar('PUBLIC_CLERK_JS_URL', context), + clerkUiUrl: getContextEnvVar('PUBLIC_CLERK_UI_URL', context), clerkJsVariant: getContextEnvVar('PUBLIC_CLERK_JS_VARIANT', context) as 'headless' | '' | undefined, clerkJsVersion: getContextEnvVar('PUBLIC_CLERK_JS_VERSION', context), apiVersion: getContextEnvVar('CLERK_API_VERSION', context), diff --git a/packages/astro/src/stores/external.ts b/packages/astro/src/stores/external.ts index bdebe805116..7532fc69fd5 100644 --- a/packages/astro/src/stores/external.ts +++ b/packages/astro/src/stores/external.ts @@ -1,6 +1,6 @@ import { deriveState } from '@clerk/shared/deriveState'; import { eventMethodCalled } from '@clerk/shared/telemetry'; -import type { SignedInSessionResource } from '@clerk/types'; +import type { SignedInSessionResource } from '@clerk/shared/types'; import { batched, computed, onMount, type Store } from 'nanostores'; import { $clerk, $csrState, $initialState } from './internal'; @@ -70,7 +70,7 @@ export const $organizationStore = computed([$authStore], auth => auth.organizati * It is a nanostore, for instructions on how to use nanostores please review the [documentation](https://github.com/nanostores/nanostores) * * @example - * $clientStore.subscribe((client) => console.log(client.activeSessions)) + * $clientStore.subscribe((client) => console.log(client?.signedInSessions?.length)) */ export const $clientStore = computed([$csrState], csr => csr.client); diff --git a/packages/astro/src/stores/internal.ts b/packages/astro/src/stores/internal.ts index 4f9a9d50dc4..383cf38e2cc 100644 --- a/packages/astro/src/stores/internal.ts +++ b/packages/astro/src/stores/internal.ts @@ -5,7 +5,7 @@ import type { OrganizationResource, SignedInSessionResource, UserResource, -} from '@clerk/types'; +} from '@clerk/shared/types'; import { atom, map } from 'nanostores'; export const $csrState = map<{ diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts index 4636b5aa317..9de65b01579 100644 --- a/packages/astro/src/types.ts +++ b/packages/astro/src/types.ts @@ -5,11 +5,15 @@ import type { MultiDomainAndOrProxyPrimitives, ProtectProps, Without, -} from '@clerk/types'; +} from '@clerk/shared/types'; +import type { ClerkUiConstructor } from '@clerk/shared/ui'; +import type { Appearance, Ui } from '@clerk/ui/internal'; -type AstroClerkUpdateOptions = Pick; +type AstroClerkUpdateOptions = Pick & { + appearance?: Appearance; +}; -type AstroClerkIntegrationParams = Without< +type AstroClerkIntegrationParams = Without< ClerkOptions, | 'isSatellite' | 'sdkMetadata' @@ -20,18 +24,30 @@ type AstroClerkIntegrationParams = Without< | 'routerPush' | 'polling' | 'touchSession' + | 'appearance' > & - MultiDomainAndOrProxyPrimitives; + MultiDomainAndOrProxyPrimitives & { + appearance?: Appearance; + clerkJSUrl?: string; + clerkJSVariant?: 'headless' | ''; + clerkJSVersion?: string; + /** + * The URL that `@clerk/ui` should be hot-loaded from. + */ + clerkUiUrl?: string; + }; -type AstroClerkCreateInstanceParams = AstroClerkIntegrationParams & { publishableKey: string }; +type AstroClerkCreateInstanceParams = AstroClerkIntegrationParams & { + publishableKey: string; +}; -// Copied from `@clerk/clerk-react` +// Copied from `@clerk/react` export interface HeadlessBrowserClerk extends Clerk { load: (opts?: Without) => Promise; updateClient: (client: ClientResource) => void; } -// Copied from `@clerk/clerk-react` +// Copied from `@clerk/react` export interface BrowserClerk extends HeadlessBrowserClerk { onComponentsReady: Promise; components: any; @@ -42,6 +58,7 @@ declare global { __astro_clerk_component_props: Map>>; __astro_clerk_function_props: Map>>; Clerk: BrowserClerk; + __internal_ClerkUiCtor?: ClerkUiConstructor; } } diff --git a/packages/backend/package.json b/packages/backend/package.json index 48d1eb8489d..2198a390157 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -110,7 +110,6 @@ }, "dependencies": { "@clerk/shared": "workspace:^", - "@clerk/types": "workspace:^", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "catalog:repo" @@ -123,7 +122,7 @@ "vitest-environment-miniflare": "2.14.4" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" }, "publishConfig": { "access": "public" diff --git a/packages/backend/src/__tests__/createRedirect.test.ts b/packages/backend/src/__tests__/createRedirect.test.ts index ab23730aca6..ee4fb558d16 100644 --- a/packages/backend/src/__tests__/createRedirect.test.ts +++ b/packages/backend/src/__tests__/createRedirect.test.ts @@ -1,4 +1,4 @@ -import type { SessionStatusClaim } from '@clerk/types'; +import type { SessionStatusClaim } from '@clerk/shared/types'; import { describe, expect, it, vi } from 'vitest'; import { createRedirect } from '../createRedirect'; diff --git a/packages/backend/src/api/endpoints/AllowlistIdentifierApi.ts b/packages/backend/src/api/endpoints/AllowlistIdentifierApi.ts index 9d51be554bc..62611e92e8e 100644 --- a/packages/backend/src/api/endpoints/AllowlistIdentifierApi.ts +++ b/packages/backend/src/api/endpoints/AllowlistIdentifierApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { AllowlistIdentifier } from '../resources/AllowlistIdentifier'; diff --git a/packages/backend/src/api/endpoints/BillingApi.ts b/packages/backend/src/api/endpoints/BillingApi.ts index c14d3a5edf5..3561a7cc9f3 100644 --- a/packages/backend/src/api/endpoints/BillingApi.ts +++ b/packages/backend/src/api/endpoints/BillingApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { BillingPlan } from '../resources/CommercePlan'; diff --git a/packages/backend/src/api/endpoints/BlocklistIdentifierApi.ts b/packages/backend/src/api/endpoints/BlocklistIdentifierApi.ts index ee6199bf912..86756c73942 100644 --- a/packages/backend/src/api/endpoints/BlocklistIdentifierApi.ts +++ b/packages/backend/src/api/endpoints/BlocklistIdentifierApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { BlocklistIdentifier } from '../resources/BlocklistIdentifier'; diff --git a/packages/backend/src/api/endpoints/ClientApi.ts b/packages/backend/src/api/endpoints/ClientApi.ts index f04202dff38..13d313cf449 100644 --- a/packages/backend/src/api/endpoints/ClientApi.ts +++ b/packages/backend/src/api/endpoints/ClientApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { Client } from '../resources/Client'; diff --git a/packages/backend/src/api/endpoints/InvitationApi.ts b/packages/backend/src/api/endpoints/InvitationApi.ts index f869234ec99..51962256ce7 100644 --- a/packages/backend/src/api/endpoints/InvitationApi.ts +++ b/packages/backend/src/api/endpoints/InvitationApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { PaginatedResourceResponse } from '../resources/Deserializer'; diff --git a/packages/backend/src/api/endpoints/JwtTemplatesApi.ts b/packages/backend/src/api/endpoints/JwtTemplatesApi.ts index 66e3dc72a0f..65beca638b3 100644 --- a/packages/backend/src/api/endpoints/JwtTemplatesApi.ts +++ b/packages/backend/src/api/endpoints/JwtTemplatesApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from 'src/util/path'; import type { DeletedObject, JwtTemplate } from '../resources'; diff --git a/packages/backend/src/api/endpoints/MachineApi.ts b/packages/backend/src/api/endpoints/MachineApi.ts index 800295a5dc8..21bb4ff133a 100644 --- a/packages/backend/src/api/endpoints/MachineApi.ts +++ b/packages/backend/src/api/endpoints/MachineApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { PaginatedResourceResponse } from '../resources/Deserializer'; diff --git a/packages/backend/src/api/endpoints/OAuthApplicationsApi.ts b/packages/backend/src/api/endpoints/OAuthApplicationsApi.ts index 641ff786ce1..37acf982456 100644 --- a/packages/backend/src/api/endpoints/OAuthApplicationsApi.ts +++ b/packages/backend/src/api/endpoints/OAuthApplicationsApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { DeletedObject } from '../resources'; diff --git a/packages/backend/src/api/endpoints/OrganizationApi.ts b/packages/backend/src/api/endpoints/OrganizationApi.ts index 5f2a0ea9c8b..ce73d0349d4 100644 --- a/packages/backend/src/api/endpoints/OrganizationApi.ts +++ b/packages/backend/src/api/endpoints/OrganizationApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest, OrganizationEnrollmentMode } from '@clerk/types'; +import type { ClerkPaginationRequest, OrganizationEnrollmentMode } from '@clerk/shared/types'; import { runtime } from '../../runtime'; import { joinPaths } from '../../util/path'; diff --git a/packages/backend/src/api/endpoints/SamlConnectionApi.ts b/packages/backend/src/api/endpoints/SamlConnectionApi.ts index 69e6ac120a5..dcad7b612a2 100644 --- a/packages/backend/src/api/endpoints/SamlConnectionApi.ts +++ b/packages/backend/src/api/endpoints/SamlConnectionApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest, SamlIdpSlug } from '@clerk/types'; +import type { ClerkPaginationRequest, SamlIdpSlug } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { SamlConnection } from '../resources'; diff --git a/packages/backend/src/api/endpoints/SessionApi.ts b/packages/backend/src/api/endpoints/SessionApi.ts index b2180f7d126..a4fc70ddf55 100644 --- a/packages/backend/src/api/endpoints/SessionApi.ts +++ b/packages/backend/src/api/endpoints/SessionApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest, SessionStatus } from '@clerk/types'; +import type { ClerkPaginationRequest, SessionStatus } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; import type { Cookies } from '../resources/Cookies'; diff --git a/packages/backend/src/api/endpoints/UserApi.ts b/packages/backend/src/api/endpoints/UserApi.ts index d1408f9e5c5..f78517ed3b4 100644 --- a/packages/backend/src/api/endpoints/UserApi.ts +++ b/packages/backend/src/api/endpoints/UserApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest, OAuthProvider, OrganizationInvitationStatus } from '@clerk/types'; +import type { ClerkPaginationRequest, OAuthProvider, OrganizationInvitationStatus } from '@clerk/shared/types'; import { runtime } from '../../runtime'; import { joinPaths } from '../../util/path'; diff --git a/packages/backend/src/api/endpoints/WaitlistEntryApi.ts b/packages/backend/src/api/endpoints/WaitlistEntryApi.ts index 7dc7b7d3f24..2d09db2d525 100644 --- a/packages/backend/src/api/endpoints/WaitlistEntryApi.ts +++ b/packages/backend/src/api/endpoints/WaitlistEntryApi.ts @@ -1,4 +1,4 @@ -import type { ClerkPaginationRequest } from '@clerk/types'; +import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from 'src/util/path'; import type { DeletedObject } from '../resources/DeletedObject'; diff --git a/packages/backend/src/api/request.ts b/packages/backend/src/api/request.ts index c9d321f505e..b9a99f283cc 100644 --- a/packages/backend/src/api/request.ts +++ b/packages/backend/src/api/request.ts @@ -1,5 +1,5 @@ import { ClerkAPIResponseError, parseError } from '@clerk/shared/error'; -import type { ClerkAPIError, ClerkAPIErrorJSON } from '@clerk/types'; +import type { ClerkAPIError, ClerkAPIErrorJSON } from '@clerk/shared/types'; import snakecaseKeys from 'snakecase-keys'; import { API_URL, API_VERSION, constants, SUPPORTED_BAPI_VERSION, USER_AGENT } from '../constants'; diff --git a/packages/backend/src/api/resources/Client.ts b/packages/backend/src/api/resources/Client.ts index c58e2aba146..1ad7a4786fe 100644 --- a/packages/backend/src/api/resources/Client.ts +++ b/packages/backend/src/api/resources/Client.ts @@ -1,4 +1,4 @@ -import type { LastAuthenticationStrategy } from '@clerk/types'; +import type { LastAuthenticationStrategy } from '@clerk/shared/types'; import type { ClientJSON } from './JSON'; import { Session } from './Session'; diff --git a/packages/backend/src/api/resources/CommerceSubscription.ts b/packages/backend/src/api/resources/CommerceSubscription.ts index 64d04089170..7c2dd2b61bb 100644 --- a/packages/backend/src/api/resources/CommerceSubscription.ts +++ b/packages/backend/src/api/resources/CommerceSubscription.ts @@ -1,4 +1,4 @@ -import type { BillingMoneyAmount } from '@clerk/types'; +import type { BillingMoneyAmount } from '@clerk/shared/types'; import { BillingSubscriptionItem } from './CommerceSubscriptionItem'; import type { BillingSubscriptionJSON } from './JSON'; diff --git a/packages/backend/src/api/resources/CommerceSubscriptionItem.ts b/packages/backend/src/api/resources/CommerceSubscriptionItem.ts index 6d312c5675f..a0f636b7b5d 100644 --- a/packages/backend/src/api/resources/CommerceSubscriptionItem.ts +++ b/packages/backend/src/api/resources/CommerceSubscriptionItem.ts @@ -1,4 +1,4 @@ -import type { BillingMoneyAmount, BillingMoneyAmountJSON } from '@clerk/types'; +import type { BillingMoneyAmount, BillingMoneyAmountJSON } from '@clerk/shared/types'; import { BillingPlan } from './CommercePlan'; import type { BillingSubscriptionItemJSON } from './JSON'; diff --git a/packages/backend/src/api/resources/Enums.ts b/packages/backend/src/api/resources/Enums.ts index c42e4a31a14..c75c70f8026 100644 --- a/packages/backend/src/api/resources/Enums.ts +++ b/packages/backend/src/api/resources/Enums.ts @@ -1,4 +1,4 @@ -import type { OrganizationCustomRoleKey } from '@clerk/types'; +import type { OrganizationCustomRoleKey } from '@clerk/shared/types'; export type OAuthProvider = | 'facebook' diff --git a/packages/backend/src/api/resources/IdPOAuthAccessToken.ts b/packages/backend/src/api/resources/IdPOAuthAccessToken.ts index 9adc870ad31..0798c264119 100644 --- a/packages/backend/src/api/resources/IdPOAuthAccessToken.ts +++ b/packages/backend/src/api/resources/IdPOAuthAccessToken.ts @@ -1,4 +1,4 @@ -import type { JwtPayload } from '@clerk/types'; +import type { JwtPayload } from '@clerk/shared/types'; import type { IdPOAuthAccessTokenJSON } from './JSON'; diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 4aee90821ea..d2cdd0c7b6a 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -1,4 +1,4 @@ -import type { LastAuthenticationStrategy, SignUpStatus, VerificationStatus } from '@clerk/types'; +import type { LastAuthenticationStrategy, SignUpStatus, VerificationStatus } from '@clerk/shared/types'; import type { ActorTokenStatus, @@ -50,7 +50,6 @@ export const ObjectType = { PhoneNumber: 'phone_number', ProxyCheck: 'proxy_check', RedirectUrl: 'redirect_url', - SamlAccount: 'saml_account', SamlConnection: 'saml_connection', Session: 'session', SignInAttempt: 'sign_in_attempt', @@ -235,21 +234,6 @@ export interface JwtTemplateJSON extends ClerkResourceJSON { created_at: number; updated_at: number; } - -export interface SamlAccountJSON extends ClerkResourceJSON { - object: typeof ObjectType.SamlAccount; - provider: string; - provider_user_id: string | null; - active: boolean; - email_address: string; - first_name: string; - last_name: string; - verification: VerificationJSON | null; - saml_connection: SamlAccountConnectionJSON | null; - last_authenticated_at: number | null; - enterprise_connection_id: string | null; -} - export interface IdentificationLinkJSON extends ClerkResourceJSON { type: string; } @@ -601,7 +585,6 @@ export interface UserJSON extends ClerkResourceJSON { web3_wallets: Web3WalletJSON[]; organization_memberships: OrganizationMembershipJSON[] | null; external_accounts: ExternalAccountJSON[]; - saml_accounts: SamlAccountJSON[]; password_last_updated_at: number | null; public_metadata: UserPublicMetadata; private_metadata: UserPrivateMetadata; @@ -734,20 +717,6 @@ export interface PermissionJSON extends ClerkResourceJSON { updated_at: number; } -export interface SamlAccountConnectionJSON extends ClerkResourceJSON { - id: string; - name: string; - domain: string; - active: boolean; - provider: string; - sync_user_attributes: boolean; - allow_subdomains: boolean; - allow_idp_initiated: boolean; - disable_additional_identifications: boolean; - created_at: number; - updated_at: number; -} - export interface MachineJSON extends ClerkResourceJSON { object: typeof ObjectType.Machine; id: string; diff --git a/packages/backend/src/api/resources/SamlAccount.ts b/packages/backend/src/api/resources/SamlAccount.ts deleted file mode 100644 index 60714aa13aa..00000000000 --- a/packages/backend/src/api/resources/SamlAccount.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { SamlAccountJSON } from './JSON'; -import { SamlAccountConnection } from './SamlConnection'; -import { Verification } from './Verification'; - -/** - * The Backend `SamlAccount` object describes a SAML account. - */ -export class SamlAccount { - constructor( - /** - * The unique identifier for the SAML account. - */ - readonly id: string, - /** - * The provider of the SAML account. - */ - readonly provider: string, - /** - * The user's ID as used in the provider. - */ - readonly providerUserId: string | null, - /** - * A boolean that indicates whether the SAML account is active. - */ - readonly active: boolean, - /** - * The email address of the SAML account. - */ - readonly emailAddress: string, - /** - * The first name of the SAML account. - */ - readonly firstName: string, - /** - * The last name of the SAML account. - */ - readonly lastName: string, - /** - * The verification of the SAML account. - */ - readonly verification: Verification | null, - /** - * The SAML connection of the SAML account. - */ - readonly samlConnection: SamlAccountConnection | null, - /** - * The date when the SAML account was last authenticated. - */ - readonly lastAuthenticatedAt: number | null, - /** - * The ID of the enterprise connection associated with this SAML account. - */ - readonly enterpriseConnectionId: string | null, - ) {} - - static fromJSON(data: SamlAccountJSON): SamlAccount { - return new SamlAccount( - data.id, - data.provider, - data.provider_user_id, - data.active, - data.email_address, - data.first_name, - data.last_name, - data.verification && Verification.fromJSON(data.verification), - data.saml_connection && SamlAccountConnection.fromJSON(data.saml_connection), - data.last_authenticated_at ?? null, - data.enterprise_connection_id, - ); - } -} diff --git a/packages/backend/src/api/resources/SamlConnection.ts b/packages/backend/src/api/resources/SamlConnection.ts index dafb4bd79f7..be8711012ca 100644 --- a/packages/backend/src/api/resources/SamlConnection.ts +++ b/packages/backend/src/api/resources/SamlConnection.ts @@ -1,4 +1,4 @@ -import type { AttributeMappingJSON, SamlAccountConnectionJSON, SamlConnectionJSON } from './JSON'; +import type { AttributeMappingJSON, SamlConnectionJSON } from './JSON'; /** * The Backend `SamlConnection` object holds information about a SAML connection for an organization. @@ -117,35 +117,6 @@ export class SamlConnection { } } -export class SamlAccountConnection { - constructor( - readonly id: string, - readonly name: string, - readonly domain: string, - readonly active: boolean, - readonly provider: string, - readonly syncUserAttributes: boolean, - readonly allowSubdomains: boolean, - readonly allowIdpInitiated: boolean, - readonly createdAt: number, - readonly updatedAt: number, - ) {} - static fromJSON(data: SamlAccountConnectionJSON): SamlAccountConnection { - return new SamlAccountConnection( - data.id, - data.name, - data.domain, - data.active, - data.provider, - data.sync_user_attributes, - data.allow_subdomains, - data.allow_idp_initiated, - data.created_at, - data.updated_at, - ); - } -} - class AttributeMapping { constructor( /** diff --git a/packages/backend/src/api/resources/SignUpAttempt.ts b/packages/backend/src/api/resources/SignUpAttempt.ts index 62707b24bb0..7426f25de1c 100644 --- a/packages/backend/src/api/resources/SignUpAttempt.ts +++ b/packages/backend/src/api/resources/SignUpAttempt.ts @@ -1,4 +1,4 @@ -import type { SignUpStatus } from '@clerk/types'; +import type { SignUpStatus } from '@clerk/shared/types'; import type { SignUpVerificationNextAction } from './Enums'; import type { SignUpJSON, SignUpVerificationJSON, SignUpVerificationsJSON } from './JSON'; diff --git a/packages/backend/src/api/resources/User.ts b/packages/backend/src/api/resources/User.ts index 8cef33ca06d..d3556aedeff 100644 --- a/packages/backend/src/api/resources/User.ts +++ b/packages/backend/src/api/resources/User.ts @@ -1,8 +1,7 @@ import { EmailAddress } from './EmailAddress'; import { ExternalAccount } from './ExternalAccount'; -import type { ExternalAccountJSON, SamlAccountJSON, UserJSON } from './JSON'; +import type { ExternalAccountJSON, UserJSON } from './JSON'; import { PhoneNumber } from './PhoneNumber'; -import { SamlAccount } from './SamlAccount'; import { Web3Wallet } from './Web3Wallet'; /** @@ -120,10 +119,6 @@ export class User { * An array of all the `ExternalAccount` objects associated with the user via OAuth. **Note**: This includes both verified & unverified external accounts. */ readonly externalAccounts: ExternalAccount[] = [], - /** - * An array of all the `SamlAccount` objects associated with the user via SAML. - */ - readonly samlAccounts: SamlAccount[] = [], /** * Date when the user was last active. */ @@ -179,7 +174,6 @@ export class User { (data.phone_numbers || []).map(x => PhoneNumber.fromJSON(x)), (data.web3_wallets || []).map(x => Web3Wallet.fromJSON(x)), (data.external_accounts || []).map((x: ExternalAccountJSON) => ExternalAccount.fromJSON(x)), - (data.saml_accounts || []).map((x: SamlAccountJSON) => SamlAccount.fromJSON(x)), data.last_active_at, data.create_organization_enabled, data.create_organizations_limit, diff --git a/packages/backend/src/api/resources/Verification.ts b/packages/backend/src/api/resources/Verification.ts index 7f65bc9a492..4da2489071e 100644 --- a/packages/backend/src/api/resources/Verification.ts +++ b/packages/backend/src/api/resources/Verification.ts @@ -1,4 +1,4 @@ -import type { VerificationStatus } from '@clerk/types'; +import type { VerificationStatus } from '@clerk/shared/types'; import type { OrganizationDomainVerificationJSON, VerificationJSON } from './JSON'; diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 855fb43df45..00e119dbb21 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -20,9 +20,13 @@ export type { SignInStatus, } from './Enums'; -export type { SignUpStatus } from '@clerk/types'; +export type { SignUpStatus } from '@clerk/shared/types'; +export * from './CommercePlan'; +export * from './CommerceSubscription'; +export * from './CommerceSubscriptionItem'; export * from './ExternalAccount'; +export * from './Feature'; export * from './IdentificationLink'; export * from './IdPOAuthAccessToken'; export * from './Instance'; @@ -30,11 +34,11 @@ export * from './InstanceRestrictions'; export * from './InstanceSettings'; export * from './Invitation'; export * from './JSON'; +export * from './JwtTemplate'; +export * from './M2MToken'; export * from './Machine'; export * from './MachineScope'; export * from './MachineSecretKey'; -export * from './M2MToken'; -export * from './JwtTemplate'; export * from './OauthAccessToken'; export * from './OAuthApplication'; export * from './Organization'; @@ -45,7 +49,6 @@ export * from './OrganizationSettings'; export * from './PhoneNumber'; export * from './ProxyCheck'; export * from './RedirectUrl'; -export * from './SamlAccount'; export * from './SamlConnection'; export * from './Session'; export * from './SignInTokens'; @@ -57,17 +60,13 @@ export * from './User'; export * from './Verification'; export * from './WaitlistEntry'; export * from './Web3Wallet'; -export * from './CommercePlan'; -export * from './CommerceSubscription'; -export * from './CommerceSubscriptionItem'; -export * from './Feature'; export type { EmailWebhookEvent, - OrganizationWebhookEvent, OrganizationDomainWebhookEvent, OrganizationInvitationWebhookEvent, OrganizationMembershipWebhookEvent, + OrganizationWebhookEvent, PermissionWebhookEvent, RoleWebhookEvent, SessionWebhookEvent, diff --git a/packages/backend/src/createRedirect.ts b/packages/backend/src/createRedirect.ts index 07f2f6b8123..41eb4fd7626 100644 --- a/packages/backend/src/createRedirect.ts +++ b/packages/backend/src/createRedirect.ts @@ -1,5 +1,5 @@ import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl'; -import type { SessionStatusClaim } from '@clerk/types'; +import type { SessionStatusClaim } from '@clerk/shared/types'; import { constants } from './constants'; import { errorThrower, parsePublishableKey } from './util/shared'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 5b707bb8b82..1559d61f7fb 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,6 +1,6 @@ import type { TelemetryCollectorOptions } from '@clerk/shared/telemetry'; import { TelemetryCollector } from '@clerk/shared/telemetry'; -import type { SDKMetadata } from '@clerk/types'; +import type { SDKMetadata } from '@clerk/shared/types'; import type { ApiClient, CreateBackendApiOptions } from './api'; import { createBackendApiClient } from './api'; diff --git a/packages/backend/src/jwt/__tests__/signJwt.test.ts b/packages/backend/src/jwt/__tests__/signJwt.test.ts index 75e00499523..f163600b071 100644 --- a/packages/backend/src/jwt/__tests__/signJwt.test.ts +++ b/packages/backend/src/jwt/__tests__/signJwt.test.ts @@ -1,4 +1,4 @@ -import type { JwtPayload } from '@clerk/types'; +import type { JwtPayload } from '@clerk/shared/types'; import { beforeEach, describe, expect, it } from 'vitest'; import { diff --git a/packages/backend/src/jwt/verifyJwt.ts b/packages/backend/src/jwt/verifyJwt.ts index 1b0cd46b67f..3070ddd5d6c 100644 --- a/packages/backend/src/jwt/verifyJwt.ts +++ b/packages/backend/src/jwt/verifyJwt.ts @@ -1,4 +1,4 @@ -import type { Jwt, JwtPayload } from '@clerk/types'; +import type { Jwt, JwtPayload } from '@clerk/shared/types'; import { TokenVerificationError, TokenVerificationErrorAction, TokenVerificationErrorReason } from '../errors'; import { runtime } from '../runtime'; diff --git a/packages/backend/src/tokens/__tests__/authObjects.test.ts b/packages/backend/src/tokens/__tests__/authObjects.test.ts index 2df02870852..0e16bbb75c8 100644 --- a/packages/backend/src/tokens/__tests__/authObjects.test.ts +++ b/packages/backend/src/tokens/__tests__/authObjects.test.ts @@ -1,4 +1,4 @@ -import type { JwtPayload } from '@clerk/types'; +import type { JwtPayload } from '@clerk/shared/types'; import { describe, expect, it, vi } from 'vitest'; import { createBackendApiClient } from '../../api/factory'; diff --git a/packages/backend/src/tokens/__tests__/authStatus.test.ts b/packages/backend/src/tokens/__tests__/authStatus.test.ts index ba301bdfb05..4ecd46ffd1a 100644 --- a/packages/backend/src/tokens/__tests__/authStatus.test.ts +++ b/packages/backend/src/tokens/__tests__/authStatus.test.ts @@ -1,4 +1,4 @@ -import type { JwtPayload } from '@clerk/types'; +import type { JwtPayload } from '@clerk/shared/types'; import { describe, expect, it } from 'vitest'; import { mockTokens, mockVerificationResults } from '../../fixtures/machine'; diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index a7ce48ca0f3..205d7d1fd3a 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -9,7 +9,7 @@ import type { ServerGetTokenOptions, SessionStatusClaim, SharedSignedInAuthObjectProperties, -} from '@clerk/types'; +} from '@clerk/shared/types'; import type { APIKey, CreateBackendApiOptions, IdPOAuthAccessToken, M2MToken } from '../api'; import { createBackendApiClient } from '../api'; diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 881e5f40ca6..27205ed40b4 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -1,4 +1,4 @@ -import type { JwtPayload, PendingSessionOptions } from '@clerk/types'; +import type { JwtPayload, PendingSessionOptions } from '@clerk/shared/types'; import { constants } from '../constants'; import type { TokenVerificationErrorReason } from '../errors'; diff --git a/packages/backend/src/tokens/authenticateContext.ts b/packages/backend/src/tokens/authenticateContext.ts index 44b05ac6b14..6d9fdcb9c5a 100644 --- a/packages/backend/src/tokens/authenticateContext.ts +++ b/packages/backend/src/tokens/authenticateContext.ts @@ -1,6 +1,6 @@ import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl'; +import type { Jwt } from '@clerk/shared/types'; import { isCurrentDevAccountPortalOrigin, isLegacyDevAccountPortalOrigin } from '@clerk/shared/url'; -import type { Jwt } from '@clerk/types'; import { constants } from '../constants'; import { decodeJwt } from '../jwt/verifyJwt'; diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 863afeabaa4..0061828b026 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -1,4 +1,4 @@ -import type { JwtPayload } from '@clerk/types'; +import type { JwtPayload } from '@clerk/shared/types'; import { constants } from '../constants'; import type { TokenCarrier } from '../errors'; diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts index cc34f1bfbda..17def630280 100644 --- a/packages/backend/src/tokens/types.ts +++ b/packages/backend/src/tokens/types.ts @@ -1,5 +1,5 @@ import type { MatchFunction } from '@clerk/shared/pathToRegexp'; -import type { PendingSessionOptions } from '@clerk/types'; +import type { PendingSessionOptions } from '@clerk/shared/types'; import type { ApiClient, APIKey, IdPOAuthAccessToken, M2MToken } from '../api'; import type { diff --git a/packages/backend/typedoc.json b/packages/backend/typedoc.json index 7aeba3de0e3..904b837abec 100644 --- a/packages/backend/typedoc.json +++ b/packages/backend/typedoc.json @@ -6,7 +6,7 @@ "./src/tokens/verify.ts", "./src/tokens/request.ts", "./src/tokens/types.ts", - "./src/tokens/authOjbects.ts", + "./src/tokens/authObjects.ts", "./src/api/resources/index.ts", "./src/api/resources/Deserializer.ts" ] diff --git a/packages/chrome-extension/package.json b/packages/chrome-extension/package.json index 4b75c451924..9b129d335f5 100644 --- a/packages/chrome-extension/package.json +++ b/packages/chrome-extension/package.json @@ -50,8 +50,9 @@ }, "dependencies": { "@clerk/clerk-js": "workspace:^", - "@clerk/clerk-react": "workspace:^", + "@clerk/react": "workspace:^", "@clerk/shared": "workspace:^", + "@clerk/ui": "workspace:^", "webextension-polyfill": "~0.12.0" }, "devDependencies": { @@ -64,7 +65,7 @@ "react-dom": "catalog:peer-react" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" }, "publishConfig": { "access": "public" diff --git a/packages/chrome-extension/src/background/clerk.ts b/packages/chrome-extension/src/background/clerk.ts index c77b4722651..57c68d5097d 100644 --- a/packages/chrome-extension/src/background/clerk.ts +++ b/packages/chrome-extension/src/background/clerk.ts @@ -1,4 +1,4 @@ -import { Clerk } from '@clerk/clerk-js/no-rhc'; +import type { Clerk } from '@clerk/clerk-js/no-rhc'; import { createClerkClient as _createClerkClient, @@ -6,8 +6,6 @@ import { } from '../internal'; import { SCOPE } from '../types'; -Clerk.mountComponentRenderer = undefined; - export type CreateClerkClientOptions = Omit<_CreateClerkClientOptions, 'scope'>; export async function createClerkClient(opts: CreateClerkClientOptions): Promise { diff --git a/packages/chrome-extension/src/index.ts b/packages/chrome-extension/src/index.ts index ec4cc5aa03e..96fa19a9706 100644 --- a/packages/chrome-extension/src/index.ts +++ b/packages/chrome-extension/src/index.ts @@ -2,9 +2,9 @@ export * from './react/re-exports'; export type { StorageCache } from './internal/utils/storage'; -// The order matters since we want override @clerk/clerk-react components +// The order matters since we want override @clerk/react components export { ClerkProvider, GoogleOneTap } from './react'; // Override Clerk React error thrower to show that errors come from @clerk/chrome-extension -import { setErrorThrowerOptions } from '@clerk/clerk-react/internal'; +import { setErrorThrowerOptions } from '@clerk/react/internal'; setErrorThrowerOptions({ packageName: PACKAGE_NAME }); diff --git a/packages/chrome-extension/src/internal/clerk.ts b/packages/chrome-extension/src/internal/clerk.ts index f5231d6c040..bcd6362a529 100644 --- a/packages/chrome-extension/src/internal/clerk.ts +++ b/packages/chrome-extension/src/internal/clerk.ts @@ -28,14 +28,16 @@ export type CreateClerkClientOptions = { syncHost?: string; }; -export async function createClerkClient({ +export function createClerkClient({ __experimental_syncHostListener = false, publishableKey, scope, storageCache = BrowserStorageCache, syncHost, -}: CreateClerkClientOptions): Promise { +}: CreateClerkClientOptions) { if (scope === SCOPE.BACKGROUND) { + // TODO @nikos + // @ts-expect-error will be replaced by clerk ui Clerk.mountComponentRenderer = undefined; } @@ -64,7 +66,7 @@ export async function createClerkClient({ const url = syncHost ? syncHost : DEFAULT_LOCAL_HOST_PERMISSION; // Create Clerk instance - clerk = new Clerk(publishableKey); + clerk = new Clerk(publishableKey, {}); // @ts-expect-error - TODO: sync is evaluating to true vs boolean const jwtOptions: JWTHandlerParams = { @@ -92,8 +94,8 @@ export async function createClerkClient({ listener?.add(); } - clerk.__unstable__onAfterResponse(responseHandler(jwt, { isProd })); - clerk.__unstable__onBeforeRequest(requestHandler(jwt, { isProd })); + clerk.__internal_onAfterResponse(responseHandler(jwt, { isProd })); + clerk.__internal_onBeforeRequest(requestHandler(jwt, { isProd })); return clerk; } diff --git a/packages/chrome-extension/src/internal/utils/request-handler.ts b/packages/chrome-extension/src/internal/utils/request-handler.ts index 0cad4178c46..448755df3bf 100644 --- a/packages/chrome-extension/src/internal/utils/request-handler.ts +++ b/packages/chrome-extension/src/internal/utils/request-handler.ts @@ -3,7 +3,7 @@ import type { Clerk } from '@clerk/clerk-js'; import { AUTH_HEADER } from '../constants'; import type { JWTHandler } from './jwt-handler'; -type Handler = Parameters[0]; +type Handler = Parameters[0]; type Req = Parameters[0]; /** Append the JWT to the FAPI request */ diff --git a/packages/chrome-extension/src/internal/utils/response-handler.ts b/packages/chrome-extension/src/internal/utils/response-handler.ts index 7e872d054b5..9a5c161e952 100644 --- a/packages/chrome-extension/src/internal/utils/response-handler.ts +++ b/packages/chrome-extension/src/internal/utils/response-handler.ts @@ -3,7 +3,7 @@ import type { Clerk } from '@clerk/clerk-js'; import { AUTH_HEADER } from '../constants'; import type { JWTHandler } from './jwt-handler'; -type Handler = Parameters[0]; +type Handler = Parameters[0]; type Res = Parameters[1]; /** Retrieve the JWT to the FAPI response */ diff --git a/packages/chrome-extension/src/react/ClerkProvider.tsx b/packages/chrome-extension/src/react/ClerkProvider.tsx index ba9ad71f50a..45237484496 100644 --- a/packages/chrome-extension/src/react/ClerkProvider.tsx +++ b/packages/chrome-extension/src/react/ClerkProvider.tsx @@ -1,12 +1,14 @@ import type { Clerk } from '@clerk/clerk-js/no-rhc'; -import type { ClerkProviderProps as ClerkReactProviderProps } from '@clerk/clerk-react'; -import { ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react'; +import type { ClerkProviderProps as ClerkReactProviderProps } from '@clerk/react'; +import { ClerkProvider as ClerkReactProvider } from '@clerk/react'; +import type { Ui } from '@clerk/react/internal'; +import { ClerkUi } from '@clerk/ui/entry'; import React from 'react'; import { createClerkClient } from '../internal/clerk'; import type { StorageCache } from '../internal/utils/storage'; -type ChromeExtensionClerkProviderProps = ClerkReactProviderProps & { +type ChromeExtensionClerkProviderProps = ClerkReactProviderProps & { /** * @experimental * @description Enables the listener to sync host cookies on changes. @@ -16,18 +18,14 @@ type ChromeExtensionClerkProviderProps = ClerkReactProviderProps & { syncHost?: string; }; -export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null { +export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null { const { children, storageCache, syncHost, __experimental_syncHostListener, ...rest } = props; const { publishableKey = '' } = props; const [clerkInstance, setClerkInstance] = React.useState(null); React.useEffect(() => { - void (async () => { - setClerkInstance( - await createClerkClient({ publishableKey, storageCache, syncHost, __experimental_syncHostListener }), - ); - })(); + setClerkInstance(createClerkClient({ publishableKey, storageCache, syncHost, __experimental_syncHostListener })); }, [publishableKey, storageCache, syncHost, __experimental_syncHostListener]); if (!clerkInstance) { @@ -38,6 +36,7 @@ export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Ele {children} diff --git a/packages/chrome-extension/src/react/re-exports.ts b/packages/chrome-extension/src/react/re-exports.ts index 4c4ca13d711..2838dc6264b 100644 --- a/packages/chrome-extension/src/react/re-exports.ts +++ b/packages/chrome-extension/src/react/re-exports.ts @@ -39,4 +39,4 @@ export { useSignIn, useSignUp, useUser, -} from '@clerk/clerk-react'; +} from '@clerk/react'; diff --git a/packages/chrome-extension/tsconfig.json b/packages/chrome-extension/tsconfig.json index 904ccc83d88..274fd384521 100644 --- a/packages/chrome-extension/tsconfig.json +++ b/packages/chrome-extension/tsconfig.json @@ -9,8 +9,8 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "preserve", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", diff --git a/packages/chrome-extension/tsup.config.ts b/packages/chrome-extension/tsup.config.ts index 6a75787567b..03130188e9f 100644 --- a/packages/chrome-extension/tsup.config.ts +++ b/packages/chrome-extension/tsup.config.ts @@ -16,7 +16,7 @@ export default defineConfig(overrideOptions => { sourcemap: true, legacyOutput: true, treeshake: true, - noExternal: ['@clerk/clerk-react', '@clerk/shared'], + noExternal: ['@clerk/react', '@clerk/shared'], external: ['use-sync-external-store', '@stripe/stripe-js', '@stripe/react-stripe-js'], define: { PACKAGE_NAME: `"${name}"`, diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 3425146261d..cbc3539426c 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,36 +1,17 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "840KB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "83KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "127KB" }, + { "path": "./dist/clerk.js", "maxSize": "538KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "63KB" }, + { "path": "./dist/clerk.chips.browser.js", "maxSize": "63KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "105KB" }, + { "path": "./dist/clerk.no-rhc.js", "maxSize": "305KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "65KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "119KB" }, - { "path": "./dist/ui-common*.legacy.*.js", "maxSize": "122KB" }, - { "path": "./dist/vendors*.js", "maxSize": "47KB" }, - { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, + { "path": "./dist/vendors*.js", "maxSize": "7KB" }, + { "path": "./dist/coinbase*.js", "maxSize": "36KB" }, + { "path": "./dist/base-account-sdk*.js", "maxSize": "203KB" }, { "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" }, - { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, - { "path": "./dist/impersonationfab*.js", "maxSize": "5KB" }, - { "path": "./dist/organizationprofile*.js", "maxSize": "10KB" }, - { "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" }, - { "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" }, - { "path": "./dist/signin*.js", "maxSize": "18KB" }, - { "path": "./dist/signup*.js", "maxSize": "9.5KB" }, - { "path": "./dist/userbutton*.js", "maxSize": "5KB" }, - { "path": "./dist/userprofile*.js", "maxSize": "16KB" }, - { "path": "./dist/userverification*.js", "maxSize": "5KB" }, - { "path": "./dist/onetap*.js", "maxSize": "1KB" }, - { "path": "./dist/waitlist*.js", "maxSize": "1.5KB" }, - { "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" }, - { "path": "./dist/enableOrganizationsPrompt*.js", "maxSize": "6.5KB" }, - { "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" }, - { "path": "./dist/checkout*.js", "maxSize": "8.82KB" }, - { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, - { "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" }, - { "path": "./dist/up-plans-page*.js", "maxSize": "1.0KB" }, - { "path": "./dist/op-plans-page*.js", "maxSize": "1.0KB" }, - { "path": "./dist/statement-page*.js", "maxSize": "1.0KB" }, - { "path": "./dist/payment-attempt-page*.js", "maxSize": "3.0KB" }, - { "path": "./dist/sessionTasks*.js", "maxSize": "3.0KB" } + { "path": "./dist/query-core-vendors*.js", "maxSize": "11KB" }, + { "path": "./dist/zxcvbn-ts-core*.js", "maxSize": "12KB" }, + { "path": "./dist/zxcvbn-common*.js", "maxSize": "226KB" } ] } diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 2d46da3cc89..ea61408c38e 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -32,7 +32,6 @@ ], "scripts": { "build": "pnpm build:bundle && pnpm build:declarations", - "postbuild": "node ../../scripts/search-for-rhc.mjs file dist/clerk.no-rhc.mjs", "build:analyze": "rspack build --config rspack.config.js --env production --env variant=\"clerk.browser\" --env analysis --analyze", "build:bundle": "pnpm clean && rspack build --config rspack.config.js --env production", "build:declarations": "tsc -p tsconfig.declarations.json", @@ -51,7 +50,8 @@ "lint": "eslint src", "lint:attw": "attw --pack . --profile node16 --ignore-rules named-exports", "lint:publint": "publint || true", - "test": "vitest --watch=false", + "postbuild:disabled": "node ../../scripts/search-for-rhc.mjs file dist/clerk.no-rhc.mjs", + "test:disabled": "vitest --watch=false", "test:sandbox:integration": "playwright test", "test:sandbox:integration:ui": "playwright test --ui", "test:sandbox:integration:update-snapshots": "playwright test --update-snapshots", @@ -59,37 +59,26 @@ }, "browserslist": "last 2 years", "dependencies": { - "@base-org/account": "2.0.1", - "@clerk/localizations": "workspace:^", + "@base-org/account": "catalog:module-manager", "@clerk/shared": "workspace:^", - "@coinbase/wallet-sdk": "4.3.0", - "@emotion/cache": "11.11.0", - "@emotion/react": "11.11.1", - "@floating-ui/react": "0.27.12", - "@floating-ui/react-dom": "^2.1.3", - "@formkit/auto-animate": "^0.8.2", + "@coinbase/wallet-sdk": "catalog:module-manager", "@stripe/stripe-js": "5.6.0", "@swc/helpers": "^0.5.17", "@tanstack/query-core": "5.87.4", - "@zxcvbn-ts/core": "3.0.4", - "@zxcvbn-ts/language-common": "3.0.4", + "@zxcvbn-ts/core": "catalog:module-manager", + "@zxcvbn-ts/language-common": "catalog:module-manager", "alien-signals": "2.0.6", "browser-tabs-lock": "1.3.0", - "copy-to-clipboard": "3.3.3", "core-js": "3.41.0", "crypto-js": "^4.2.0", "dequal": "2.0.3", - "input-otp": "1.4.2", - "qrcode.react": "4.2.0", "regenerator-runtime": "0.14.1" }, "devDependencies": { "@clerk/testing": "workspace:^", "@rsdoctor/rspack-plugin": "^0.4.13", - "@rspack/cli": "^1.4.11", - "@rspack/core": "^1.4.11", - "@rspack/plugin-react-refresh": "^1.5.0", - "@svgr/webpack": "^6.5.1", + "@rspack/cli": "^1.6.0", + "@rspack/core": "^1.6.0", "@types/cloudflare-turnstile": "^0.2.2", "@types/node": "^22.18.12", "@types/webpack-env": "^1.18.8", @@ -98,12 +87,8 @@ "minimatch": "^10.0.3", "webpack-merge": "^5.10.0" }, - "peerDependencies": { - "react": "catalog:peer-react", - "react-dom": "catalog:peer-react" - }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" }, "publishConfig": { "access": "public" diff --git a/packages/clerk-js/rspack.config.js b/packages/clerk-js/rspack.config.js index 9b082397aa8..9c79f6b156c 100644 --- a/packages/clerk-js/rspack.config.js +++ b/packages/clerk-js/rspack.config.js @@ -5,6 +5,7 @@ const path = require('path'); const { merge } = require('webpack-merge'); const ReactRefreshPlugin = require('@rspack/plugin-react-refresh'); const { RsdoctorRspackPlugin } = require('@rsdoctor/rspack-plugin'); +const { svgLoader, typescriptLoaderProd, typescriptLoaderDev } = require('../../scripts/rspack-common'); const isProduction = mode => mode === 'production'; const isDevelopment = mode => !isProduction(mode); @@ -120,20 +121,6 @@ const common = ({ mode, variant, disableRHC = false }) => { chunks: 'all', enforce: true, }, - /** - * Sign up is shared between the SignUp component and the SignIn component. - */ - signUp: { - minChunks: 1, - name: 'signup', - test: module => !!(module.resource && module.resource.includes('/ui/components/SignUp')), - }, - common: { - minChunks: 1, - name: 'ui-common', - priority: -20, - test: module => !!(module.resource && !module.resource.includes('/ui/components')), - }, defaultVendors: { minChunks: 1, test: /[\\/]node_modules[\\/]/, @@ -155,116 +142,6 @@ const common = ({ mode, variant, disableRHC = false }) => { }; }; -/** @type { () => (import('@rspack/core').RuleSetRule) } */ -const svgLoader = () => { - return { - test: /\.svg$/, - resolve: { - fullySpecified: false, - }, - use: { - loader: '@svgr/webpack', - options: { - svgo: true, - svgoConfig: { - floatPrecision: 3, - transformPrecision: 1, - plugins: ['preset-default', 'removeDimensions', 'removeStyleElement'], - }, - }, - }, - }; -}; - -/** @type { (opts?: { targets?: string, useCoreJs?: boolean }) => (import('@rspack/core').RuleSetRule[]) } */ -const typescriptLoaderProd = ( - { targets = packageJSON.browserslist, useCoreJs = false } = { targets: packageJSON.browserslist, useCoreJs: false }, -) => { - return [ - { - test: /\.(jsx?|tsx?)$/, - exclude: /node_modules/, - use: { - loader: 'builtin:swc-loader', - options: { - env: { - targets, - ...(useCoreJs - ? { - mode: 'usage', - coreJs: require('core-js/package.json').version, - } - : {}), - }, - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - externalHelpers: true, - transform: { - react: { - runtime: 'automatic', - importSource: '@emotion/react', - development: false, - refresh: false, - }, - }, - }, - }, - }, - }, - { - test: /\.m?js$/, - exclude: /node_modules[\\/]core-js/, - use: { - loader: 'builtin:swc-loader', - options: { - env: { - targets, - ...(useCoreJs - ? { - mode: 'usage', - coreJs: '3.41.0', - } - : {}), - }, - isModule: 'unknown', - }, - }, - }, - ]; -}; - -/** @type { () => (import('@rspack/core').RuleSetRule[]) } */ -const typescriptLoaderDev = () => { - return [ - { - test: /\.(jsx?|tsx?)$/, - exclude: /node_modules/, - loader: 'builtin:swc-loader', - options: { - jsc: { - target: 'esnext', - parser: { - syntax: 'typescript', - tsx: true, - }, - externalHelpers: true, - transform: { - react: { - runtime: 'automatic', - importSource: '@emotion/react', - development: true, - refresh: true, - }, - }, - }, - }, - }, - ]; -}; - /** * Used for production builds that have dynamicly loaded chunks. * @type { (opts?: { targets?: string, useCoreJs?: boolean }) => (import('@rspack/core').Configuration) } @@ -608,7 +485,7 @@ const devConfig = ({ mode, env }) => { cache: true, experiments: { cache: { - type: 'persistent', + type: 'memory', }, }, }; diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 1382070c66a..97cd8722af9 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -196,7 +196,7 @@ function appearanceVariableOptions() { }); const updateVariables = () => { - void Clerk.__unstable__updateProps({ + void Clerk.__internal_updateProps({ appearance: { // Preserve existing appearance properties like baseTheme ...Clerk.__internal_getOption('appearance'), @@ -241,7 +241,7 @@ function otherOptions() { }); const updateOtherOptions = () => { - void Clerk.__unstable__updateProps({ + void Clerk.__internal_updateProps({ options: Object.fromEntries( Object.entries(otherOptionsInputs).map(([key, input]) => { sessionStorage.setItem(key, input.value); @@ -316,7 +316,7 @@ void (async () => { Clerk.mountWaitlist(app, componentControls.waitlist.getProps() ?? {}); }, '/keyless': () => { - void Clerk.__unstable__updateProps({ + void Clerk.__internal_updateProps({ options: { __internal_keyless_claimKeylessApplicationUrl: 'https://dashboard.clerk.com', __internal_keyless_copyInstanceKeysUrl: 'https://dashboard.clerk.com', diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index ccf7ca9a091..de0562da1bf 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -180,8 +180,8 @@ describe('Clerk singleton', () => { eventBusSpy?.mockRestore(); // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; + (window as any).__internal_onBeforeSetActive = null; + (window as any).__internal_onAfterSetActive = null; }); it('does not call session touch on signOut', async () => { @@ -219,11 +219,11 @@ describe('Clerk singleton', () => { }); }); - it('calls __unstable__onBeforeSetActive before session.touch', async () => { + it('calls __internal_onBeforeSetActive before session.touch', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - (window as any).__unstable__onBeforeSetActive = () => { + (window as any).__internal_onBeforeSetActive = () => { expect(mockSession.touch).not.toHaveBeenCalled(); }; @@ -233,11 +233,11 @@ describe('Clerk singleton', () => { expect(mockSession.touch).toHaveBeenCalled(); }); - it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => { + it('sets __session and __client_uat cookie before calling __internal_onBeforeSetActive', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - (window as any).__unstable__onBeforeSetActive = () => { + (window as any).__internal_onBeforeSetActive = () => { expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken }); }; @@ -246,100 +246,6 @@ describe('Clerk singleton', () => { await sut.setActive({ session: mockSession as any as ActiveSessionResource }); }); - it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => { - const beforeEmitMock = vi.fn(); - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - - (window as any).__unstable__onAfterSetActive = () => { - expect(mockSession.touch).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalled(); - }; - - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); - }); - - // TODO: @dimkl include set transitive state - it('calls session.touch -> set cookie -> before emit with touched session on session switch', async () => { - const mockSession2 = { - id: '2', - remove: vi.fn(), - status: 'active', - user: {}, - touch: vi.fn(), - getToken: vi.fn(), - }; - mockClientFetch.mockReturnValue( - Promise.resolve({ - signedInSessions: [mockSession, mockSession2], - }), - ); - - const sut = new Clerk(productionPublishableKey); - await sut.load(); - - const executionOrder: string[] = []; - mockSession2.touch.mockImplementationOnce(() => { - sut.session = mockSession2 as any; - executionOrder.push('session.touch'); - return Promise.resolve(); - }); - mockSession2.getToken.mockImplementation(() => { - executionOrder.push('set cookie'); - return 'mocked-token-2'; - }); - const beforeEmitMock = vi.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); - }); - - await sut.setActive({ session: mockSession2 as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); - - await waitFor(() => { - expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); - expect(mockSession2.touch).toHaveBeenCalled(); - expect(mockSession2.getToken).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2); - expect(sut.session).toMatchObject(mockSession2); - }); - }); - - // TODO: @dimkl include set transitive state - it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - - const executionOrder: string[] = []; - mockSession.touch.mockImplementationOnce(() => { - sut.session = mockSession as any; - executionOrder.push('session.touch'); - return Promise.resolve(); - }); - mockSession.getToken.mockImplementation(() => { - executionOrder.push('set cookie'); - return 'mocked-token'; - }); - - const beforeEmitMock = vi.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); - }); - - await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); - - await waitFor(() => { - expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); - expect(mockSession.touch).toHaveBeenCalled(); - expect(mockSession.getToken).toHaveBeenCalled(); - expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); - expect(sut.session).toMatchObject(mockSession); - }); - }); - it('sets active organization by slug', async () => { const mockSession2 = { id: '1', @@ -465,24 +371,16 @@ describe('Clerk singleton', () => { const sut = new Clerk(productionPublishableKey); await sut.load({ standardBrowser: false }); - const executionOrder: string[] = []; mockSession.touch.mockImplementationOnce(() => { sut.session = mockSession as any; - executionOrder.push('session.touch'); - return Promise.resolve(); - }); - const beforeEmitMock = vi.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); return Promise.resolve(); }); - await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); + await sut.setActive({ organization: { id: 'org_id' } as Organization }); - expect(executionOrder).toEqual(['session.touch', 'before emit']); expect(mockSession.touch).toHaveBeenCalled(); expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); expect(mockSession.getToken).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); expect(sut.session).toMatchObject(mockSession); }); }); @@ -523,8 +421,8 @@ describe('Clerk singleton', () => { eventBusSpy?.mockRestore(); // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; + (window as any).__internal_onBeforeSetActive = null; + (window as any).__internal_onAfterSetActive = null; }); it('calls session.touch by default', async () => { @@ -537,12 +435,12 @@ describe('Clerk singleton', () => { expect(mockSession.touch).toHaveBeenCalled(); }); - it('does not call __unstable__onBeforeSetActive before session.touch', async () => { + it('does not call __internal_onBeforeSetActive before session.touch', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const onBeforeSetActive = vi.fn(); - (window as any).__unstable__onBeforeSetActive = onBeforeSetActive; + (window as any).__internal_onBeforeSetActive = onBeforeSetActive; const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -550,12 +448,12 @@ describe('Clerk singleton', () => { expect(onBeforeSetActive).not.toHaveBeenCalled(); }); - it('does not call __unstable__onAfterSetActive after session.touch', async () => { + it('does not call __internal_onAfterSetActive after session.touch', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); const onAfterSetActive = vi.fn(); - (window as any).__unstable__onAfterSetActive = onAfterSetActive; + (window as any).__internal_onAfterSetActive = onAfterSetActive; const sut = new Clerk(productionPublishableKey); await sut.load(); @@ -624,8 +522,8 @@ describe('Clerk singleton', () => { mockEnvironmentFetch.mockReset(); // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; + (window as any).__internal_onBeforeSetActive = null; + (window as any).__internal_onAfterSetActive = null; }); it('does not update session to personal workspace', async () => { @@ -688,8 +586,8 @@ describe('Clerk singleton', () => { afterEach(() => { // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; + (window as any).__internal_onBeforeSetActive = null; + (window as any).__internal_onAfterSetActive = null; }); it('gracefully handles an incorrect value returned from the user provided selectInitialSession', async () => { @@ -2454,15 +2352,15 @@ describe('Clerk singleton', () => { describe('updateClient', () => { afterEach(() => { // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; + (window as any).__internal_onBeforeSetActive = null; + (window as any).__internal_onAfterSetActive = null; }); it('runs server revalidation hooks when session transitions from `active` to `pending`', async () => { const mockOnBeforeSetActive = vi.fn().mockReturnValue(Promise.resolve()); const mockOnAfterSetActive = vi.fn().mockReturnValue(Promise.resolve()); - (window as any).__unstable__onBeforeSetActive = mockOnBeforeSetActive; - (window as any).__unstable__onAfterSetActive = mockOnAfterSetActive; + (window as any).__internal_onBeforeSetActive = mockOnBeforeSetActive; + (window as any).__internal_onAfterSetActive = mockOnAfterSetActive; const mockActiveSession = { id: 'session_1', diff --git a/packages/clerk-js/src/core/auth/CaptchaHeartbeat.ts b/packages/clerk-js/src/core/auth/CaptchaHeartbeat.ts index 9acabcbb667..ae67e78f4d6 100644 --- a/packages/clerk-js/src/core/auth/CaptchaHeartbeat.ts +++ b/packages/clerk-js/src/core/auth/CaptchaHeartbeat.ts @@ -35,7 +35,7 @@ export class CaptchaHeartbeat { } private isEnabled() { - return !!this.clerk.__unstable__environment?.displayConfig?.captchaHeartbeat; + return !!this.clerk.__internal_environment?.displayConfig?.captchaHeartbeat; } private clientBypass() { @@ -43,6 +43,6 @@ export class CaptchaHeartbeat { } private intervalInMs() { - return this.clerk.__unstable__environment?.displayConfig?.captchaHeartbeatIntervalMs ?? 10 * 60 * 1000; + return this.clerk.__internal_environment?.displayConfig?.captchaHeartbeatIntervalMs ?? 10 * 60 * 1000; } } diff --git a/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts b/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts index 889cca10513..1f48f4e4ee0 100644 --- a/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts +++ b/packages/clerk-js/src/core/auth/cookies/__tests__/clientUat.test.ts @@ -1,15 +1,15 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; +import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { inCrossOriginIframe } from '../../../../utils'; import { getCookieDomain } from '../../getCookieDomain'; import { getSecureAttribute } from '../../getSecureAttribute'; import { createClientUatCookie } from '../clientUat'; vi.mock('@clerk/shared/cookie'); vi.mock('@clerk/shared/date'); -vi.mock('../../../../utils'); +vi.mock('@clerk/shared/internal/clerk-js/runtime'); vi.mock('../../getCookieDomain'); vi.mock('../../getSecureAttribute'); diff --git a/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts b/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts index 6248d78b9eb..d51bf732de9 100644 --- a/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts +++ b/packages/clerk-js/src/core/auth/cookies/__tests__/session.test.ts @@ -1,14 +1,14 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; +import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { inCrossOriginIframe } from '../../../../utils'; import { getSecureAttribute } from '../../getSecureAttribute'; import { createSessionCookie } from '../session'; vi.mock('@clerk/shared/cookie'); vi.mock('@clerk/shared/date'); -vi.mock('../../../../utils'); +vi.mock('@clerk/shared/internal/clerk-js/runtime'); vi.mock('../../getSecureAttribute'); describe('createSessionCookie', () => { diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index a6d11bdd5a7..20a620938b8 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -1,9 +1,9 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; +import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { getSuffixedCookieName } from '@clerk/shared/keys'; import type { ClientResource } from '@clerk/shared/types'; -import { inCrossOriginIframe } from '../../../utils'; import { getCookieDomain } from '../getCookieDomain'; import { getSecureAttribute } from '../getSecureAttribute'; diff --git a/packages/clerk-js/src/core/auth/cookies/devBrowser.ts b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts index 47edda5fadd..ab3f66eb6d4 100644 --- a/packages/clerk-js/src/core/auth/cookies/devBrowser.ts +++ b/packages/clerk-js/src/core/auth/cookies/devBrowser.ts @@ -1,9 +1,9 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; import { DEV_BROWSER_JWT_KEY } from '@clerk/shared/devBrowser'; +import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { getSuffixedCookieName } from '@clerk/shared/keys'; -import { inCrossOriginIframe } from '../../../utils'; import { getSecureAttribute } from '../getSecureAttribute'; export type DevBrowserCookieHandler = { @@ -12,10 +12,10 @@ export type DevBrowserCookieHandler = { remove: () => void; }; -const getCookieAttributes = (): { sameSite: string; secure: boolean } => { +const getCookieAttributes = () => { const sameSite = inCrossOriginIframe() ? 'None' : 'Lax'; const secure = getSecureAttribute(sameSite); - return { sameSite, secure }; + return { sameSite, secure } as const; }; /** diff --git a/packages/clerk-js/src/core/auth/cookies/session.ts b/packages/clerk-js/src/core/auth/cookies/session.ts index 209755d96f4..e4362a2522d 100644 --- a/packages/clerk-js/src/core/auth/cookies/session.ts +++ b/packages/clerk-js/src/core/auth/cookies/session.ts @@ -1,8 +1,8 @@ import { createCookieHandler } from '@clerk/shared/cookie'; import { addYears } from '@clerk/shared/date'; +import { inCrossOriginIframe } from '@clerk/shared/internal/clerk-js/runtime'; import { getSuffixedCookieName } from '@clerk/shared/keys'; -import { inCrossOriginIframe } from '../../../utils'; import { getSecureAttribute } from '../getSecureAttribute'; const SESSION_COOKIE_NAME = '__session'; @@ -13,11 +13,11 @@ export type SessionCookieHandler = { get: () => string | undefined; }; -const getCookieAttributes = (): { sameSite: string; secure: boolean; partitioned: boolean } => { +const getCookieAttributes = () => { const sameSite = __BUILD_VARIANT_CHIPS__ ? 'None' : inCrossOriginIframe() ? 'None' : 'Lax'; const secure = getSecureAttribute(sameSite); const partitioned = __BUILD_VARIANT_CHIPS__ && secure; - return { sameSite, secure, partitioned }; + return { sameSite, secure, partitioned } as const; }; /** diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 66c90865c04..e529e6d55e1 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,6 +1,5 @@ import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; import { clerkEvents, createClerkEventBus } from '@clerk/shared/clerkEventBus'; -import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, EmailLinkError, @@ -9,6 +8,30 @@ import { isClerkAPIResponseError, isClerkRuntimeError, } from '@clerk/shared/error'; +import { + disabledAllAPIKeysFeatures, + disabledAllBillingFeatures, + disabledOrganizationAPIKeysFeature, + disabledOrganizationsFeature, + disabledUserAPIKeysFeature, + isSignedInAndSingleSessionModeEnabled, + noOrganizationExists, + noUserExists, +} from '@clerk/shared/internal/clerk-js/componentGuards'; +import { + CLERK_SATELLITE_URL, + CLERK_SUFFIXED_COOKIES, + CLERK_SYNCED, + ERROR_CODES, +} from '@clerk/shared/internal/clerk-js/constants'; +import { RedirectUrls } from '@clerk/shared/internal/clerk-js/redirectUrls'; +import { + getTaskEndpoint, + navigateIfTaskExists, + warnMissingPendingTaskHandlers, +} from '@clerk/shared/internal/clerk-js/sessionTasks'; +import { warnings } from '@clerk/shared/internal/clerk-js/warnings'; +import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate'; import { parsePublishableKey } from '@clerk/shared/keys'; import { logger } from '@clerk/shared/logger'; import { CLERK_NETLIFY_CACHE_BUST_PARAM } from '@clerk/shared/netlifyCacheHandler'; @@ -20,7 +43,6 @@ import { TelemetryCollector, } from '@clerk/shared/telemetry'; import type { - __experimental_CheckoutInstance, __experimental_CheckoutOptions, __internal_AttemptToEnableEnvironmentSettingParams, __internal_AttemptToEnableEnvironmentSettingResult, @@ -38,6 +60,7 @@ import type { AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, BillingNamespace, + CheckoutSignalValue, Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, @@ -96,13 +119,14 @@ import type { WaitlistResource, Web3Provider, } from '@clerk/shared/types'; +import type { ClerkUi } from '@clerk/shared/ui'; import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; import { allSettled, handleValueOrFn, noop } from '@clerk/shared/utils'; import type { QueryClient } from '@tanstack/query-core'; import { debugLogger, initDebugLogger } from '@/utils/debug'; +import { ModuleManager } from '@/utils/moduleManager'; -import type { MountComponentRenderer } from '../ui/Components'; import { ALLOWED_PROTOCOLS, buildURL, @@ -110,18 +134,8 @@ import { createAllowedRedirectOrigins, createBeforeUnloadTracker, createPageLifecycle, - disabledAllAPIKeysFeatures, - disabledAllBillingFeatures, - disabledOrganizationAPIKeysFeature, - disabledOrganizationsFeature, - disabledUserAPIKeysFeature, errorThrower, - generateSignatureWithBase, - generateSignatureWithCoinbaseWallet, - generateSignatureWithMetamask, - generateSignatureWithOKXWallet, getClerkQueryParam, - getWeb3Identifier, hasExternalAccountSignUpError, inActiveBrowserTab, inBrowser, @@ -129,22 +143,15 @@ import { isError, isOrganizationId, isRedirectForFAPIInitiatedFlow, - isSignedInAndSingleSessionModeEnabled, - noOrganizationExists, - noUserExists, - processCssLayerNameExtraction, removeClerkQueryParam, requiresUserInput, stripOrigin, - windowNavigate, + web3, } from '../utils'; -import { assertNoLegacyProp } from '../utils/assertNoLegacyProp'; import { CLERK_ENVIRONMENT_STORAGE_ENTRY, SafeLocalStorage } from '../utils/localStorage'; import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback'; -import { RedirectUrls } from '../utils/redirectUrls'; import { AuthCookieService } from './auth/AuthCookieService'; import { CaptchaHeartbeat } from './auth/CaptchaHeartbeat'; -import { CLERK_SATELLITE_URL, CLERK_SUFFIXED_COOKIES, CLERK_SYNCED, ERROR_CODES } from './constants'; import { clerkErrorInitFailed, clerkInvalidSignInUrlFormat, @@ -164,9 +171,7 @@ import { Billing } from './modules/billing'; import { createCheckoutInstance } from './modules/checkout/instance'; import { Protect } from './protect'; import { BaseResource, Client, Environment, Organization, Waitlist } from './resources/internal'; -import { getTaskEndpoint, navigateIfTaskExists, warnMissingPendingTaskHandlers } from './sessionTasks'; import { State } from './state'; -import { warnings } from './warnings'; type SetActiveHook = (intent?: 'sign-out') => void | Promise; @@ -203,8 +208,6 @@ const defaultOptions: ClerkOptions = { }; export class Clerk implements ClerkInterface { - public static mountComponentRenderer?: MountComponentRenderer; - public static version: string = __PKG_VERSION__; public static sdkMetadata: SDKMetadata = { name: __PKG_NAME__, @@ -235,7 +238,7 @@ export class Clerk implements ClerkInterface { #protect?: Protect; #captchaHeartbeat?: CaptchaHeartbeat; #broadcastChannel: BroadcastChannel | null = null; - #componentControls?: ReturnType | null; + #clerkUi?: Promise; //@ts-expect-error with being undefined even though it's not possible - related to issue with ts and error thrower #fapiClient: FapiClient; #instanceType?: InstanceType; @@ -380,9 +383,9 @@ export class Clerk implements ClerkInterface { return Clerk._apiKeys; } - __experimental_checkout(options: __experimental_CheckoutOptions): __experimental_CheckoutInstance { + __experimental_checkout(options: __experimental_CheckoutOptions): CheckoutSignalValue { if (!this._checkout) { - this._checkout = params => createCheckoutInstance(this, params); + this._checkout = (params: any) => createCheckoutInstance(this, params); } return this._checkout(options); } @@ -455,6 +458,19 @@ export class Clerk implements ClerkInterface { this.#options = this.#initOptions(options); + // Initialize ClerkUi if it was provided + if (this.#options.clerkUiCtor) { + this.#clerkUi = Promise.resolve(this.#options.clerkUiCtor).then( + ClerkUI => + new ClerkUI( + () => this, + () => this.environment, + this.#options, + new ModuleManager(), + ), + ); + } + // In development mode, if custom router options are provided, warn if both routerPush and routerReplace are not provided if ( this.#instanceType === 'development' && @@ -479,8 +495,6 @@ export class Clerk implements ClerkInterface { this.#emit(); }); - assertNoLegacyProp(this.#options); - if (this.#options.sdkMetadata) { Clerk.sdkMetadata = this.#options.sdkMetadata; } @@ -543,13 +557,13 @@ export class Clerk implements ClerkInterface { } const onBeforeSetActive: SetActiveHook = - typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function' - ? window.__unstable__onBeforeSetActive + typeof window !== 'undefined' && typeof window.__internal_onBeforeSetActive === 'function' + ? window.__internal_onBeforeSetActive : noop; const onAfterSetActive: SetActiveHook = - typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function' - ? window.__unstable__onAfterSetActive + typeof window !== 'undefined' && typeof window.__internal_onAfterSetActive === 'function' + ? window.__internal_onAfterSetActive : noop; const opts = callbackOrOptions && typeof callbackOrOptions === 'object' ? callbackOrOptions : options || {}; @@ -625,22 +639,18 @@ export class Clerk implements ClerkInterface { }; public openGoogleOneTap = (props?: GoogleOneTapProps): void => { + this.assertComponentsReady(this.#clerkUi); const component = 'GoogleOneTap'; - this.assertComponentsReady(this.#componentControls); - void this.#componentControls - .ensureMounted({ preloadHint: component }) - .then(controls => controls.openModal('googleOneTap', props || {})); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openModal('googleOneTap', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened(component, props)); }; public closeGoogleOneTap = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('googleOneTap')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('googleOneTap')); }; public openSignIn = (props?: SignInProps): void => { - this.assertComponentsReady(this.#componentControls); if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { @@ -649,22 +659,19 @@ export class Clerk implements ClerkInterface { } return; } + this.assertComponentsReady(this.#clerkUi); const component = 'SignIn'; - void this.#componentControls - .ensureMounted({ preloadHint: component }) - .then(controls => controls.openModal('signIn', props || {})); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openModal('signIn', props || {})); const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() }; this.telemetry?.record(eventPrebuiltComponentOpened(component, props, additionalData)); }; public closeSignIn = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('signIn')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('signIn')); }; public __internal_openCheckout = (props?: __internal_CheckoutProps): void => { - this.assertComponentsReady(this.#componentControls); if (disabledAllBillingFeatures(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyBillingComponent('Checkout'), { @@ -682,18 +689,15 @@ export class Clerk implements ClerkInterface { return; } - void this.#componentControls - .ensureMounted({ preloadHint: 'Checkout' }) - .then(controls => controls.openDrawer('checkout', props || {})); + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openDrawer('checkout', props || {})); }; public __internal_closeCheckout = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeDrawer('checkout')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeDrawer('checkout')); }; public __internal_openPlanDetails = (props: __internal_PlanDetailsProps): void => { - this.assertComponentsReady(this.#componentControls); if (disabledAllBillingFeatures(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyBillingComponent('PlanDetails'), { @@ -702,33 +706,29 @@ export class Clerk implements ClerkInterface { } return; } + this.assertComponentsReady(this.#clerkUi); const component = 'PlanDetails'; - void this.#componentControls - .ensureMounted({ preloadHint: component }) - .then(controls => controls.openDrawer('planDetails', props || {})); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openDrawer('planDetails', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened(component, props)); }; public __internal_closePlanDetails = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeDrawer('planDetails')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeDrawer('planDetails')); }; public __internal_openSubscriptionDetails = (props?: __internal_SubscriptionDetailsProps): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls - .ensureMounted({ preloadHint: 'SubscriptionDetails' }) + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi + .then(ui => ui.ensureMounted()) .then(controls => controls.openDrawer('subscriptionDetails', props || {})); }; public __internal_closeSubscriptionDetails = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeDrawer('subscriptionDetails')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeDrawer('subscriptionDetails')); }; public __internal_openReverification = (props?: __internal_UserVerificationModalProps): void => { - this.assertComponentsReady(this.#componentControls); if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, { @@ -737,16 +737,16 @@ export class Clerk implements ClerkInterface { } return; } - void this.#componentControls - .ensureMounted({ preloadHint: 'UserVerification' }) + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi + .then(ui => ui.ensureMounted()) .then(controls => controls.openModal('userVerification', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened(`UserVerification`, props)); }; public __internal_closeReverification = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('userVerification')); }; public __internal_attemptToEnableEnvironmentSetting = ( @@ -790,31 +790,29 @@ export class Clerk implements ClerkInterface { }; public __internal_openEnableOrganizationsPrompt = (props: __internal_EnableOrganizationsPromptProps): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls - .ensureMounted({ preloadHint: 'EnableOrganizationsPrompt' }) + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi + .then(ui => ui.ensureMounted({ preloadHint: 'EnableOrganizationsPrompt' })) .then(controls => controls.openModal('enableOrganizationsPrompt', props || {})); this.telemetry?.record(eventPrebuiltComponentMounted('EnableOrganizationsPrompt', props)); }; public __internal_closeEnableOrganizationsPrompt = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('enableOrganizationsPrompt')); + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi + ?.then(ui => ui.ensureMounted()) + .then(controls => controls.closeModal('enableOrganizationsPrompt')); }; public __internal_openBlankCaptchaModal = (): Promise => { - this.assertComponentsReady(this.#componentControls); - return this.#componentControls - .ensureMounted({ preloadHint: 'BlankCaptchaModal' }) - .then(controls => controls.openModal('blankCaptcha', {})); + this.assertComponentsReady(this.#clerkUi); + return this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openModal('blankCaptcha', {})); }; public __internal_closeBlankCaptchaModal = (): Promise => { - this.assertComponentsReady(this.#componentControls); - return this.#componentControls - .ensureMounted({ preloadHint: 'BlankCaptchaModal' }) - .then(controls => controls.closeModal('blankCaptcha')); + this.assertComponentsReady(this.#clerkUi); + return this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('blankCaptcha')); }; public __internal_loadStripeJs = async () => { @@ -828,7 +826,6 @@ export class Clerk implements ClerkInterface { }; public openSignUp = (props?: SignUpProps): void => { - this.assertComponentsReady(this.#componentControls); if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { @@ -837,20 +834,17 @@ export class Clerk implements ClerkInterface { } return; } - void this.#componentControls - .ensureMounted({ preloadHint: 'SignUp' }) - .then(controls => controls.openModal('signUp', props || {})); + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openModal('signUp', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('SignUp', props)); }; public closeSignUp = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('signUp')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('signUp')); }; public openUserProfile = (props?: UserProfileProps): void => { - this.assertComponentsReady(this.#componentControls); if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, { @@ -859,22 +853,18 @@ export class Clerk implements ClerkInterface { } return; } - void this.#componentControls - .ensureMounted({ preloadHint: 'UserProfile' }) - .then(controls => controls.openModal('userProfile', props || {})); + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openModal('userProfile', props || {})); const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined; this.telemetry?.record(eventPrebuiltComponentOpened('UserProfile', props, additionalData)); }; public closeUserProfile = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userProfile')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('userProfile')); }; public openOrganizationProfile = (props?: OrganizationProfileProps): void => { - this.assertComponentsReady(this.#componentControls); - const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationProfile', @@ -897,21 +887,20 @@ export class Clerk implements ClerkInterface { } return; } - void this.#componentControls - .ensureMounted({ preloadHint: 'OrganizationProfile' }) + + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi + .then(ui => ui.ensureMounted()) .then(controls => controls.openModal('organizationProfile', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('OrganizationProfile', props)); }; public closeOrganizationProfile = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('organizationProfile')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('organizationProfile')); }; public openCreateOrganization = (props?: CreateOrganizationProps): void => { - this.assertComponentsReady(this.#componentControls); - const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'CreateOrganization', @@ -926,107 +915,94 @@ export class Clerk implements ClerkInterface { return; } - void this.#componentControls - .ensureMounted({ preloadHint: 'CreateOrganization' }) + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi + .then(ui => ui.ensureMounted()) .then(controls => controls.openModal('createOrganization', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('CreateOrganization', props)); }; public closeCreateOrganization = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('createOrganization')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('createOrganization')); }; public openWaitlist = (props?: WaitlistProps): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls - .ensureMounted({ preloadHint: 'Waitlist' }) - .then(controls => controls.openModal('waitlist', props || {})); + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.openModal('waitlist', props || {})); this.telemetry?.record(eventPrebuiltComponentOpened('Waitlist', props)); }; public closeWaitlist = (): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.closeModal('waitlist')); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.closeModal('waitlist')); }; public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => { - this.assertComponentsReady(this.#componentControls); + this.assertComponentsReady(this.#clerkUi); const component = 'SignIn'; - void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls => - controls.mountComponent({ - name: component, - appearanceKey: 'signIn', - node, - props, - }), - ); + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'signIn', + node, + props, + }), + ); const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() }; this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData)); }; public unmountSignIn = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => - controls.unmountComponent({ - node, - }), - ); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountUserAvatar = (node: HTMLDivElement, props?: UserAvatarProps): void => { - this.assertComponentsReady(this.#componentControls); + this.assertComponentsReady(this.#clerkUi); const component = 'UserAvatar'; - void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls => - controls.mountComponent({ - name: component, - appearanceKey: 'userAvatar', - node, - props, - }), - ); + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'userAvatar', + node, + props, + }), + ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountUserAvatar = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => - controls.unmountComponent({ - node, - }), - ); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => { - this.assertComponentsReady(this.#componentControls); + this.assertComponentsReady(this.#clerkUi); const component = 'SignUp'; - void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls => - controls.mountComponent({ - name: component, - appearanceKey: 'signUp', - node, - props, - }), - ); + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'signUp', + node, + props, + }), + ); this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountSignUp = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => - controls.unmountComponent({ - node, - }), - ); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountUserProfile = (node: HTMLDivElement, props?: UserProfileProps): void => { - this.assertComponentsReady(this.#componentControls); if (noUserExists(this)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderComponentWhenUserDoesNotExist, { @@ -1035,32 +1011,28 @@ export class Clerk implements ClerkInterface { } return; } + this.assertComponentsReady(this.#clerkUi); const component = 'UserProfile'; - void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls => - controls.mountComponent({ - name: component, - appearanceKey: 'userProfile', - node, - props, - }), - ); + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'userProfile', + node, + props, + }), + ); const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined; this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData)); }; public unmountUserProfile = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => - controls.unmountComponent({ - node, - }), - ); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountOrganizationProfile = (node: HTMLDivElement, props?: OrganizationProfileProps) => { - this.assertComponentsReady(this.#componentControls); - const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationProfile', @@ -1084,30 +1056,28 @@ export class Clerk implements ClerkInterface { } return; } - void this.#componentControls.ensureMounted({ preloadHint: 'OrganizationProfile' }).then(controls => - controls.mountComponent({ - name: 'OrganizationProfile', - appearanceKey: 'userProfile', - node, - props, - }), - ); - this.telemetry?.record(eventPrebuiltComponentMounted('OrganizationProfile', props)); + this.assertComponentsReady(this.#clerkUi); + const component = 'OrganizationProfile'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'userProfile', + node, + props, + }), + ); + + this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountOrganizationProfile = (node: HTMLDivElement) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => - controls.unmountComponent({ - node, - }), - ); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { - this.assertComponentsReady(this.#componentControls); - const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'CreateOrganization', @@ -1122,30 +1092,27 @@ export class Clerk implements ClerkInterface { return; } - void this.#componentControls?.ensureMounted({ preloadHint: 'CreateOrganization' }).then(controls => - controls.mountComponent({ - name: 'CreateOrganization', - appearanceKey: 'createOrganization', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'CreateOrganization'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'createOrganization', + node, + props, + }), + ); - this.telemetry?.record(eventPrebuiltComponentMounted('CreateOrganization', props)); + this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountCreateOrganization = (node: HTMLDivElement) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls?.ensureMounted().then(controls => - controls.unmountComponent({ - node, - }), - ); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountOrganizationSwitcher = (node: HTMLDivElement, props?: OrganizationSwitcherProps) => { - this.assertComponentsReady(this.#componentControls); - const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationSwitcher', @@ -1160,17 +1127,21 @@ export class Clerk implements ClerkInterface { return; } - void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationSwitcher' }).then(controls => - controls.mountComponent({ - name: 'OrganizationSwitcher', - appearanceKey: 'organizationSwitcher', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'OrganizationSwitcher'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'organizationSwitcher', + node, + props, + }), + ); this.telemetry?.record( - eventPrebuiltComponentMounted('OrganizationSwitcher', { + eventPrebuiltComponentMounted(component, { ...props, forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection, }), @@ -1178,20 +1149,15 @@ export class Clerk implements ClerkInterface { }; public unmountOrganizationSwitcher = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public __experimental_prefetchOrganizationSwitcher = () => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls - ?.ensureMounted({ preloadHint: 'OrganizationSwitcher' }) - .then(controls => controls.prefetch('organizationSwitcher')); + this.assertComponentsReady(this.#clerkUi); + void this.#clerkUi.then(ui => ui.ensureMounted()).then(controls => controls.prefetch('organizationSwitcher')); }; public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => { - this.assertComponentsReady(this.#componentControls); - const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'OrganizationList', @@ -1206,17 +1172,21 @@ export class Clerk implements ClerkInterface { return; } - void this.#componentControls?.ensureMounted({ preloadHint: 'OrganizationList' }).then(controls => - controls.mountComponent({ - name: 'OrganizationList', - appearanceKey: 'organizationList', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'OrganizationList'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'organizationList', + node, + props, + }), + ); this.telemetry?.record( - eventPrebuiltComponentMounted('OrganizationList', { + eventPrebuiltComponentMounted(component, { ...props, forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection, }), @@ -1224,55 +1194,57 @@ export class Clerk implements ClerkInterface { }; public unmountOrganizationList = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountUserButton = (node: HTMLDivElement, props?: UserButtonProps) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls?.ensureMounted({ preloadHint: 'UserButton' }).then(controls => - controls.mountComponent({ - name: 'UserButton', - appearanceKey: 'userButton', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'UserButton'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'userButton', + node, + props, + }), + ); const additionalData = { ...(props?.customMenuItems?.length || 0 > 0 ? { customItems: true } : undefined), ...(props?.__experimental_asStandalone ? { standalone: true } : undefined), }; - this.telemetry?.record(eventPrebuiltComponentMounted('UserButton', props, additionalData)); + this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData)); }; public unmountUserButton = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountWaitlist = (node: HTMLDivElement, props?: WaitlistProps) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls?.ensureMounted({ preloadHint: 'Waitlist' }).then(controls => - controls.mountComponent({ - name: 'Waitlist', - appearanceKey: 'waitlist', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'Waitlist'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'waitlist', + node, + props, + }), + ); - this.telemetry?.record(eventPrebuiltComponentMounted('Waitlist', props)); + this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountWaitlist = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountPricingTable = (node: HTMLDivElement, props?: PricingTableProps): void => { - this.assertComponentsReady(this.#componentControls); if (disabledAllBillingFeatures(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotRenderAnyBillingComponent('PricingTable'), { @@ -1281,50 +1253,43 @@ export class Clerk implements ClerkInterface { } return; } - // Temporary backward compatibility for legacy prop: `forOrganizations`. Will be removed in the coming minor release. - const nextProps = { ...(props as any) } as PricingTableProps & { forOrganizations?: boolean }; - if (typeof (props as any)?.forOrganizations !== 'undefined') { - logger.warnOnce( - 'Clerk: [IMPORTANT] prop `forOrganizations` is deprecated and will be removed in the coming minors. Use `for="organization"` instead.', + this.assertComponentsReady(this.#clerkUi); + const component = 'PricingTable'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'pricingTable', + node, + props, + }), ); - } - - void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls => - controls.mountComponent({ - name: 'PricingTable', - appearanceKey: 'pricingTable', - node, - props: nextProps, - }), - ); - this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', nextProps)); + this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountPricingTable = (node: HTMLDivElement): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => - controls.unmountComponent({ - node, - }), - ); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public __internal_mountOAuthConsent = (node: HTMLDivElement, props?: __internal_OAuthConsentProps) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted({ preloadHint: 'OAuthConsent' }).then(controls => - controls.mountComponent({ - name: 'OAuthConsent', - appearanceKey: '__internal_oauthConsent', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'OAuthConsent'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: '__internal_oauthConsent', + node, + props, + }), + ); }; public __internal_unmountOAuthConsent = (node: HTMLDivElement) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; /** @@ -1335,8 +1300,6 @@ export class Clerk implements ClerkInterface { * @param props Configuration parameters. */ public mountAPIKeys = (node: HTMLDivElement, props?: APIKeysProps) => { - this.assertComponentsReady(this.#componentControls); - logger.warnOnce('Clerk: component is in early access and not yet recommended for production use.'); if (disabledAllAPIKeysFeatures(this, this.environment)) { @@ -1366,16 +1329,20 @@ export class Clerk implements ClerkInterface { return; } - void this.#componentControls.ensureMounted({ preloadHint: 'APIKeys' }).then(controls => - controls.mountComponent({ - name: 'APIKeys', - appearanceKey: 'apiKeys', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'APIKeys'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'apiKeys', + node, + props, + }), + ); - this.telemetry?.record(eventPrebuiltComponentMounted('APIKeys', props)); + this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; /** @@ -1387,13 +1354,10 @@ export class Clerk implements ClerkInterface { * @param targetNode Target node to unmount the APIKeys component from. */ public unmountAPIKeys = (node: HTMLDivElement) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => { - this.assertComponentsReady(this.#componentControls); - const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', caller: 'TaskChooseOrganization', @@ -1408,48 +1372,53 @@ export class Clerk implements ClerkInterface { return; } - void this.#componentControls.ensureMounted({ preloadHint: 'TaskChooseOrganization' }).then(controls => - controls.mountComponent({ - name: 'TaskChooseOrganization', - appearanceKey: 'taskChooseOrganization', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + const component = 'TaskChooseOrganization'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'taskChooseOrganization', + node, + props, + }), + ); - this.telemetry?.record(eventPrebuiltComponentMounted('TaskChooseOrganization', props)); + this.telemetry?.record(eventPrebuiltComponentMounted(component, props)); }; public unmountTaskChooseOrganization = (node: HTMLDivElement) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; public mountTaskResetPassword = (node: HTMLDivElement, props?: TaskResetPasswordProps) => { - this.assertComponentsReady(this.#componentControls); - - void this.#componentControls.ensureMounted({ preloadHint: 'TaskResetPassword' }).then(controls => - controls.mountComponent({ - name: 'TaskResetPassword', - appearanceKey: 'taskResetPassword', - node, - props, - }), - ); + this.assertComponentsReady(this.#clerkUi); + + const component = 'TaskResetPassword'; + void this.#clerkUi + .then(ui => ui.ensureMounted()) + .then(controls => + controls.mountComponent({ + name: component, + appearanceKey: 'taskResetPassword', + node, + props, + }), + ); this.telemetry?.record(eventPrebuiltComponentMounted('TaskResetPassword', props)); }; public unmountTaskResetPassword = (node: HTMLDivElement) => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted().then(controls => controls.unmountComponent({ node })); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; /** * `setActive` can be used to set the active session and/or organization. */ public setActive = async (params: SetActiveParams): Promise => { - const { organization, beforeEmit, redirectUrl, navigate: setActiveNavigate } = params; + const { organization, redirectUrl, navigate: setActiveNavigate } = params; let { session } = params; this.__internal_setActiveInProgress = true; debugLogger.debug( @@ -1481,13 +1450,13 @@ export class Clerk implements ClerkInterface { } const onBeforeSetActive: SetActiveHook = - typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function' - ? window.__unstable__onBeforeSetActive + typeof window !== 'undefined' && typeof window.__internal_onBeforeSetActive === 'function' + ? window.__internal_onBeforeSetActive : noop; const onAfterSetActive: SetActiveHook = - typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function' - ? window.__unstable__onAfterSetActive + typeof window !== 'undefined' && typeof window.__internal_onAfterSetActive === 'function' + ? window.__internal_onAfterSetActive : noop; let newSession = session === undefined ? this.session : session; @@ -1554,29 +1523,18 @@ export class Clerk implements ClerkInterface { eventBus.emit(events.TokenUpdate, { token: null }); } - //2. If there's a beforeEmit, typically we're navigating. Emit the session as - // undefined, then wait for beforeEmit to complete before emitting the new session. + //2. When navigation is required we emit the session as undefined, + // then wait for navigation to finish before emitting the new session. // When undefined, neither SignedIn nor SignedOut renders, which avoids flickers or // automatic reloading when reloading shouldn't be happening. const tracker = createBeforeUnloadTracker(this.#options.standardBrowser); - if (beforeEmit) { - deprecated( - 'Clerk.setActive({beforeEmit})', - 'Use the `redirectUrl` property instead. Example `Clerk.setActive({redirectUrl:"/"})`', - ); - await tracker.track(async () => { - this.#setTransitiveState(); - await beforeEmit(newSession); - }); - } - const taskUrl = newSession?.status === 'pending' && newSession?.currentTask && this.#options.taskUrls?.[newSession?.currentTask.key]; - if (!beforeEmit && (redirectUrl || taskUrl || setActiveNavigate)) { + if (redirectUrl || taskUrl || setActiveNavigate) { await tracker.track(async () => { if (!this.client) { // Typescript is not happy because since thinks this.client might have changed to undefined because the function is asynchronous. @@ -2396,20 +2354,20 @@ export class Clerk implements ClerkInterface { const { displayConfig } = this.environment; const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; - const identifier = await getWeb3Identifier({ provider }); + const identifier = await web3().getWeb3Identifier({ provider }); let generateSignature: (params: GenerateSignatureParams) => Promise; switch (provider) { case 'metamask': - generateSignature = generateSignatureWithMetamask; + generateSignature = web3().generateSignatureWithMetamask; break; case 'base': - generateSignature = generateSignatureWithBase; + generateSignature = web3().generateSignatureWithBase; break; case 'coinbase_wallet': - generateSignature = generateSignatureWithCoinbaseWallet; + generateSignature = web3().generateSignatureWithCoinbaseWallet; break; default: - generateSignature = generateSignatureWithOKXWallet; + generateSignature = web3().generateSignatureWithOKXWallet; break; } @@ -2538,8 +2496,8 @@ export class Clerk implements ClerkInterface { const hasTransitionedToPendingStatus = this.session.status === 'active' && session?.status === 'pending'; if (hasTransitionedToPendingStatus) { const onAfterSetActive: SetActiveHook = - typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function' - ? window.__unstable__onAfterSetActive + typeof window !== 'undefined' && typeof window.__internal_onAfterSetActive === 'function' + ? window.__internal_onAfterSetActive : noop; // Execute hooks to update server authentication context and trigger @@ -2564,30 +2522,31 @@ export class Clerk implements ClerkInterface { this.#emit(); }; - get __unstable__environment(): EnvironmentResource | null | undefined { + get __internal_environment(): EnvironmentResource | null | undefined { return this.environment; } // TODO: Fix this properly // eslint-disable-next-line @typescript-eslint/require-await - __unstable__setEnvironment = async (env: EnvironmentJSON) => { + __internal_setEnvironment = async (env: EnvironmentJSON) => { this.environment = new Environment(env); - if (Clerk.mountComponentRenderer) { - this.#componentControls = Clerk.mountComponentRenderer(this, this.environment, this.#options); - } + // TODO @nikos update + // if (Clerk.mountComponentRenderer) { + // this.#componentRenderer = Clerk.mountComponentRenderer(this, this.environment, this.#options); + // } }; - __unstable__onBeforeRequest = (callback: FapiRequestCallback): void => { + __internal_onBeforeRequest = (callback: FapiRequestCallback): void => { this.#fapiClient.onBeforeRequest(callback); }; - __unstable__onAfterResponse = (callback: FapiRequestCallback): void => { + __internal_onAfterResponse = (callback: FapiRequestCallback): void => { this.#fapiClient.onAfterResponse(callback); }; // TODO @userland-errors: - __unstable__updateProps = (_props: any) => { + __internal_updateProps = (_props: any) => { // We need to re-init the options here in order to keep the options passed to ClerkProvider // in sync with the state of clerk-js. If we don't init the options here again, the following scenario is possible: // 1. User renders @@ -2599,7 +2558,7 @@ export class Clerk implements ClerkInterface { options: this.#initOptions({ ...this.#options, ..._props.options }), }; - return this.#componentControls?.ensureMounted().then(controls => controls.updateProps(props)); + return this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.updateProps(props)); }; __internal_navigateWithError(to: string, err: ClerkAPIError) { @@ -2808,23 +2767,11 @@ export class Clerk implements ClerkInterface { }); }; - const initComponents = () => { - if (Clerk.mountComponentRenderer && !this.#componentControls) { - this.#componentControls = Clerk.mountComponentRenderer( - this, - this.environment as Environment, - this.#options, - ); - } - }; - const [, clientResult] = await allSettled([initEnvironmentPromise, initClient()]); - if (clientResult.status === 'rejected') { const e = clientResult.reason; if (isError(e, 'requires_captcha')) { - initComponents(); await initClient(); } else { throw e; @@ -2836,9 +2783,6 @@ export class Clerk implements ClerkInterface { if (await this.#redirectFAPIInitiatedFlow()) { return; } - - initComponents(); - break; } catch (err) { if (isError(err, 'dev_browser_unauthenticated')) { @@ -2892,17 +2836,11 @@ export class Clerk implements ClerkInterface { this.updateClient(client); this.updateEnvironment(environment); - // TODO: Add an auth service also for non standard browsers that will poll for the __session JWT but won't use cookies - - if (Clerk.mountComponentRenderer) { - this.#componentControls = Clerk.mountComponentRenderer(this, this.environment, this.#options); - } - this.#publicEventBus.emit(clerkEvents.Status, initializationDegradedCounter > 0 ? 'degraded' : 'ready'); }; - // This is used by @clerk/clerk-expo + // This is used by @clerk/expo __internal_reloadInitialResources = async (): Promise => { const [environment, client] = await Promise.all([ Environment.getInstance().fetch({ touch: false, fetchMaxTries: 1 }), @@ -3039,23 +2977,26 @@ export class Clerk implements ClerkInterface { this.addListener(({ session }) => { const isImpersonating = !!session?.actor; if (isImpersonating) { - void this.#componentControls?.ensureMounted().then(controls => controls.mountImpersonationFab()); + void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.mountImpersonationFab()); } }); }; #handleKeylessPrompt = () => { if (this.#options.__internal_keyless_claimKeylessApplicationUrl) { - void this.#componentControls?.ensureMounted().then(controls => { - // TODO(@pantelis): Investigate if this resets existing props - controls.updateProps({ - options: { - __internal_keyless_claimKeylessApplicationUrl: this.#options.__internal_keyless_claimKeylessApplicationUrl, - __internal_keyless_copyInstanceKeysUrl: this.#options.__internal_keyless_copyInstanceKeysUrl, - __internal_keyless_dismissPrompt: this.#options.__internal_keyless_dismissPrompt, - }, + void this.#clerkUi + ?.then(ui => ui.ensureMounted()) + .then(controls => { + // TODO(@pantelis): Investigate if this resets existing props + controls.updateProps({ + options: { + __internal_keyless_claimKeylessApplicationUrl: + this.#options.__internal_keyless_claimKeylessApplicationUrl, + __internal_keyless_copyInstanceKeysUrl: this.#options.__internal_keyless_copyInstanceKeysUrl, + __internal_keyless_dismissPrompt: this.#options.__internal_keyless_dismissPrompt, + }, + }); }); - }); } }; @@ -3086,12 +3027,9 @@ export class Clerk implements ClerkInterface { return this.buildUrlWithAuth(url); }; - assertComponentsReady(controls: unknown): asserts controls is ReturnType { - if (!Clerk.mountComponentRenderer) { - throw new Error('ClerkJS was loaded without UI components.'); - } - if (!controls) { - throw new Error('ClerkJS components are not ready yet.'); + assertComponentsReady(val: unknown): asserts val is ClerkUi { + if (!val) { + throw new Error('Clerk was not loaded with Ui components'); } } @@ -3120,16 +3058,9 @@ export class Clerk implements ClerkInterface { }; #initOptions = (options?: ClerkOptions): ClerkOptions => { - const processedOptions = options ? { ...options } : {}; - - // Extract cssLayerName from baseTheme if present and move it to appearance level - if (processedOptions.appearance) { - processedOptions.appearance = processCssLayerNameExtraction(processedOptions.appearance); - } - return { ...defaultOptions, - ...processedOptions, + ...options, allowedRedirectOrigins: createAllowedRedirectOrigins( options?.allowedRedirectOrigins, this.frontendApi, diff --git a/packages/clerk-js/src/core/constants.ts b/packages/clerk-js/src/core/constants.ts index c2a728c71f4..43ee8e244f7 100644 --- a/packages/clerk-js/src/core/constants.ts +++ b/packages/clerk-js/src/core/constants.ts @@ -1,58 +1,6 @@ -import type { SignUpModes } from '@clerk/shared/types'; - -// TODO: Do we still have a use for this or can we simply preserve all params? -export const PRESERVED_QUERYSTRING_PARAMS = [ - 'redirect_url', - 'after_sign_in_url', - 'after_sign_up_url', - 'sign_in_force_redirect_url', - 'sign_in_fallback_redirect_url', - 'sign_up_force_redirect_url', - 'sign_up_fallback_redirect_url', -]; - -export const CLERK_MODAL_STATE = '__clerk_modal_state'; -export const CLERK_SATELLITE_URL = '__clerk_satellite_url'; -export const CLERK_SUFFIXED_COOKIES = 'suffixed_cookies'; -export const CLERK_SYNCED = '__clerk_synced'; -export const ERROR_CODES = { - FORM_IDENTIFIER_NOT_FOUND: 'form_identifier_not_found', - FORM_PASSWORD_INCORRECT: 'form_password_incorrect', - FORM_PASSWORD_PWNED: 'form_password_pwned', - INVALID_STRATEGY_FOR_USER: 'strategy_for_user_invalid', - NOT_ALLOWED_TO_SIGN_UP: 'not_allowed_to_sign_up', - OAUTH_ACCESS_DENIED: 'oauth_access_denied', - OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: 'oauth_email_domain_reserved_by_saml', - NOT_ALLOWED_ACCESS: 'not_allowed_access', - SAML_USER_ATTRIBUTE_MISSING: 'saml_user_attribute_missing', - USER_LOCKED: 'user_locked', - EXTERNAL_ACCOUNT_NOT_FOUND: 'external_account_not_found', - SESSION_EXISTS: 'session_exists', - SIGN_UP_MODE_RESTRICTED: 'sign_up_mode_restricted', - SIGN_UP_MODE_RESTRICTED_WAITLIST: 'sign_up_restricted_waitlist', - ENTERPRISE_SSO_USER_ATTRIBUTE_MISSING: 'enterprise_sso_user_attribute_missing', - ENTERPRISE_SSO_EMAIL_ADDRESS_DOMAIN_MISMATCH: 'enterprise_sso_email_address_domain_mismatch', - ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: 'enterprise_sso_hosted_domain_mismatch', - SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: 'saml_email_address_domain_mismatch', - INVITATION_ACCOUNT_NOT_EXISTS: 'invitation_account_not_exists', - ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: 'organization_membership_quota_exceeded_for_sso', - CAPTCHA_INVALID: 'captcha_invalid', - FRAUD_DEVICE_BLOCKED: 'device_blocked', - FRAUD_ACTION_BLOCKED: 'action_blocked', - SIGNUP_RATE_LIMIT_EXCEEDED: 'signup_rate_limit_exceeded', - USER_BANNED: 'user_banned', -} as const; - -export const SIGN_IN_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username']; -export const SIGN_UP_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username', 'first_name', 'last_name']; - -export const DEBOUNCE_MS = 350; - -export const SIGN_UP_MODES = { - PUBLIC: 'public', - RESTRICTED: 'restricted', - WAITLIST: 'waitlist', -} satisfies Record; - -// This is the currently supported version of the Frontend API -export const SUPPORTED_FAPI_VERSION = '2025-11-10'; +/** + * Re-exporting constants from @clerk/shared to avoid refactoring all imports. + * The constants have been moved to @clerk/shared/internal/clerk-js/constants + * to make them available across all Clerk packages. + */ +export * from '@clerk/shared/internal/clerk-js/constants'; diff --git a/packages/clerk-js/src/core/errors.ts b/packages/clerk-js/src/core/errors.ts index 3a589781565..45005bff61e 100644 --- a/packages/clerk-js/src/core/errors.ts +++ b/packages/clerk-js/src/core/errors.ts @@ -1,129 +1 @@ -const errorPrefix = 'ClerkJS:'; - -/** - * Used to log a warning when a Clerk feature is used in an unsupported environment. - * (Development Only) - * This is a warning and not an error because the application will still work, but the feature will not be available. - * - * @param strategy The strategy that is not supported in the current environment. - * @returns void - */ -export function clerkUnsupportedEnvironmentWarning(strategy: string) { - console.warn(`${errorPrefix} ${strategy} is not supported in this environment.`); -} - -export function clerkNetworkError(url: string, e: Error): never { - throw new Error(`${errorPrefix} Network error at "${url}" - ${e}. Please try again.`); -} - -export function clerkErrorInitFailed(): never { - throw new Error(`${errorPrefix} Something went wrong initializing Clerk.`); -} - -export function clerkErrorDevInitFailed(msg = ''): never { - throw new Error(`${errorPrefix} Something went wrong initializing Clerk in development mode.${msg && ` ${msg}`}`); -} - -export function clerkErrorPathRouterMissingPath(componentName: string): never { - throw new Error( - `${errorPrefix} Missing path option. The ${componentName} component was mounted with path routing so you need to specify the path where the component is mounted on e.g. path="/sign-in".`, - ); -} - -export function clerkCoreErrorContextProviderNotFound(providerName: string): never { - throw new Error(`${errorPrefix} You must wrap your application in a <${providerName}> component.`); -} - -export function clerkCoreErrorNoClerkSingleton(): never { - throw new Error(`${errorPrefix} Clerk is undefined`); -} - -export function clerkUIErrorDOMElementNotFound(): never { - throw new Error(`${errorPrefix} The target element is empty. Provide a valid DOM element.`); -} - -export function clerkMissingFapiClientInResources(): never { - throw new Error(`${errorPrefix} Missing FAPI client in resources.`); -} - -export function clerkOAuthCallbackDidNotCompleteSignInSignUp(type: 'sign in' | 'sign up'): never { - throw new Error( - `${errorPrefix} Something went wrong initializing Clerk during the ${type} flow. Please contact support.`, - ); -} - -export function clerkVerifyEmailAddressCalledBeforeCreate(type: 'SignIn' | 'SignUp'): never { - throw new Error(`${errorPrefix} You need to start a ${type} flow by calling ${type}.create() first.`); -} - -export function clerkInvalidStrategy(functionaName: string, strategy: string): never { - throw new Error(`${errorPrefix} Strategy "${strategy}" is not a valid strategy for ${functionaName}.`); -} - -export function clerkVerifyWeb3WalletCalledBeforeCreate(type: 'SignIn' | 'SignUp'): never { - throw new Error( - `${errorPrefix} You need to start a ${type} flow by calling ${type}.create({ identifier: 'your web3 wallet address' }) first`, - ); -} - -export function clerkVerifyPasskeyCalledBeforeCreate(): never { - throw new Error( - `${errorPrefix} You need to start a SignIn flow by calling SignIn.create({ strategy: 'passkey' }) first`, - ); -} - -export function clerkMissingOptionError(name = ''): never { - throw new Error(`${errorPrefix} Missing '${name}' option`); -} - -export function clerkInvalidFAPIResponse(status: string | null, supportEmail: string): never { - throw new Error( - `${errorPrefix} Response: ${status || 0} not supported yet.\nFor more information contact us at ${supportEmail}`, - ); -} - -export function clerkMissingDevBrowserJwt(): never { - throw new Error(`${errorPrefix} Missing dev browser jwt. Please contact support.`); -} - -export function clerkMissingProxyUrlAndDomain(): never { - throw new Error( - `${errorPrefix} Missing domain and proxyUrl. A satellite application needs to specify a domain or a proxyUrl.`, - ); -} - -export function clerkInvalidSignInUrlOrigin(): never { - throw new Error(`${errorPrefix} The signInUrl needs to be on a different origin than your satellite application.`); -} - -export function clerkInvalidSignInUrlFormat(): never { - throw new Error(`${errorPrefix} The signInUrl needs to have a absolute url format.`); -} - -export function clerkMissingSignInUrlAsSatellite(): never { - throw new Error( - `${errorPrefix} Missing signInUrl. A satellite application needs to specify the signInUrl for development instances.`, - ); -} - -export function clerkRedirectUrlIsMissingScheme(): never { - throw new Error(`${errorPrefix} Invalid redirect_url. A valid http or https url should be used for the redirection.`); -} - -export function clerkFailedToLoadThirdPartyScript(name?: string): never { - throw new Error(`${errorPrefix} Unable to retrieve a third party script${name ? ` ${name}` : ''}.`); -} - -export function clerkInvalidRoutingStrategy(strategy?: string): never { - throw new Error(`${errorPrefix} Invalid routing strategy, path cannot be used in tandem with ${strategy}.`); -} - -export function clerkUnsupportedReloadMethod(className: string): never { - throw new Error(`${errorPrefix} Calling ${className}.reload is not currently supported. Please contact support.`); -} - -export function clerkMissingWebAuthnPublicKeyOptions(name: 'create' | 'get'): never { - throw new Error( - `${errorPrefix} Missing publicKey. When calling 'navigator.credentials.${name}()' it is required to pass a publicKey object.`, - ); -} +export * from '@clerk/shared/internal/clerk-js/errors'; diff --git a/packages/clerk-js/src/core/fapiClient.ts b/packages/clerk-js/src/core/fapiClient.ts index 7d03e620c27..412c708b871 100644 --- a/packages/clerk-js/src/core/fapiClient.ts +++ b/packages/clerk-js/src/core/fapiClient.ts @@ -1,16 +1,13 @@ import { isBrowserOnline } from '@clerk/shared/browser'; +import { buildEmailAddress as buildEmailAddressUtil } from '@clerk/shared/internal/clerk-js/email'; +import { stringifyQueryParams } from '@clerk/shared/internal/clerk-js/querystring'; import { retry } from '@clerk/shared/retry'; import type { ClerkAPIErrorJSON, ClientJSON, InstanceType } from '@clerk/shared/types'; import { camelToSnake } from '@clerk/shared/underscore'; import { debugLogger } from '@/utils/debug'; -import { - buildEmailAddress as buildEmailAddressUtil, - buildURL as buildUrlUtil, - filterUndefinedValues, - stringifyQueryParams, -} from '../utils'; +import { buildURL as buildUrlUtil, filterUndefinedValues } from '../utils'; import { SUPPORTED_FAPI_VERSION } from './constants'; import { clerkNetworkError } from './errors'; @@ -90,7 +87,7 @@ export function createFapiClient(options: FapiClientOptions): FapiClient { } async function runBeforeRequestCallbacks(requestInit: FapiRequestInit) { - const windowCallback = typeof window !== 'undefined' && (window as any).__unstable__onBeforeRequest; + const windowCallback = typeof window !== 'undefined' && (window as any).__internal_onBeforeRequest; for await (const callback of [windowCallback, ...onBeforeRequestCallbacks].filter(s => s)) { if ((await callback(requestInit)) === false) { return false; @@ -100,7 +97,7 @@ export function createFapiClient(options: FapiClientOptions): FapiClient { } async function runAfterResponseCallbacks(requestInit: FapiRequestInit, response: FapiResponse) { - const windowCallback = typeof window !== 'undefined' && (window as any).__unstable__onAfterResponse; + const windowCallback = typeof window !== 'undefined' && (window as any).__internal_onAfterResponse; for await (const callback of [windowCallback, ...onAfterResponseCallbacks].filter(s => s)) { if ((await callback(requestInit, response)) === false) { return false; diff --git a/packages/clerk-js/src/core/modules/checkout/__tests__/manager.test.ts b/packages/clerk-js/src/core/modules/checkout/__tests__/manager.test.ts deleted file mode 100644 index 7342174ad23..00000000000 --- a/packages/clerk-js/src/core/modules/checkout/__tests__/manager.test.ts +++ /dev/null @@ -1,714 +0,0 @@ -import type { BillingCheckoutResource, ClerkAPIResponseError } from '@clerk/shared/types'; -import type { MockedFunction } from 'vitest'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { type CheckoutCacheState, type CheckoutKey, createCheckoutManager, FETCH_STATUS } from '../manager'; - -// Type-safe mock for BillingCheckoutResource -const createMockCheckoutResource = (overrides: Partial = {}): BillingCheckoutResource => ({ - id: 'checkout_123', - status: 'needs_confirmation', - externalClientSecret: 'cs_test_123', - externalGatewayId: 'gateway_123', - totals: { - totalDueNow: { amount: 1000, currency: 'USD', currencySymbol: '$', amountFormatted: '10.00' }, - credit: { amount: 0, currency: 'USD', currencySymbol: '$', amountFormatted: '0.00' }, - pastDue: { amount: 0, currency: 'USD', currencySymbol: '$', amountFormatted: '0.00' }, - subtotal: { amount: 1000, currency: 'USD', currencySymbol: '$', amountFormatted: '10.00' }, - grandTotal: { amount: 1000, currency: 'USD', currencySymbol: '$', amountFormatted: '10.00' }, - taxTotal: { amount: 0, currency: 'USD', currencySymbol: '$', amountFormatted: '0.00' }, - }, - isImmediatePlanChange: false, - planPeriod: 'month', - freeTrialEndsAt: null, - needsPaymentMethod: true, - payer: { - id: 'payer_123', - createdAt: new Date('2025-01-01'), - updatedAt: new Date('2025-01-01'), - firstName: 'Test Payer', - lastName: 'Test Payer', - email: 'test@clerk.com', - imageUrl: 'https://example.com/avatar.png', - pathRoot: '', - reload: vi.fn(), - }, - plan: { - id: 'plan_123', - name: 'Pro Plan', - description: 'Professional plan', - features: [], - fee: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' }, - annualFee: { amount: 12000, amountFormatted: '120.00', currency: 'USD', currencySymbol: '$' }, - annualMonthlyFee: { amount: 1000, amountFormatted: '10.00', currency: 'USD', currencySymbol: '$' }, - slug: 'pro-plan', - isDefault: false, - isRecurring: true, - hasBaseFee: false, - forPayerType: 'user', - publiclyVisible: true, - freeTrialDays: 0, - freeTrialEnabled: false, - avatarUrl: '', - pathRoot: '', - reload: vi.fn(), - }, - paymentMethod: undefined, - confirm: vi.fn(), - reload: vi.fn(), - pathRoot: '/checkout', - ...overrides, -}); - -// Type-safe mock for ClerkAPIResponseError -const createMockError = (message = 'Test error'): ClerkAPIResponseError => { - const error = new Error(message) as ClerkAPIResponseError; - error.status = 400; - error.clerkTraceId = 'trace_123'; - error.clerkError = true; - return error; -}; - -// Helper to create a typed cache key -const createCacheKey = (key: string): CheckoutKey => key as CheckoutKey; - -describe('createCheckoutManager', () => { - const testCacheKey = createCacheKey('user-123-plan-456-monthly'); - let manager: ReturnType; - - beforeEach(() => { - vi.clearAllMocks(); - manager = createCheckoutManager(testCacheKey); - }); - - describe('getCacheState', () => { - it('should return default state when cache is empty', () => { - const state = manager.getCacheState(); - - expect(state).toEqual({ - isStarting: false, - isConfirming: false, - error: null, - checkout: null, - fetchStatus: 'idle', - status: 'needs_initialization', - }); - }); - - it('should return immutable state object', () => { - const state = manager.getCacheState(); - - // State should be frozen - expect(Object.isFrozen(state)).toBe(true); - }); - }); - - describe('subscribe', () => { - it('should add listener and return unsubscribe function', () => { - const listener: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - - const unsubscribe = manager.subscribe(listener); - - expect(typeof unsubscribe).toBe('function'); - expect(listener).not.toHaveBeenCalled(); - }); - - it('should remove listener when unsubscribe is called', async () => { - const listener: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - - const unsubscribe = manager.subscribe(listener); - - // Trigger a state change - const mockCheckout = createMockCheckoutResource(); - const mockOperation = vi.fn().mockResolvedValue(mockCheckout); - await manager.executeOperation('start', mockOperation); - - expect(listener).toHaveBeenCalled(); - - // Clear the mock and unsubscribe - listener.mockClear(); - unsubscribe(); - - // Trigger another state change - const anotherMockOperation = vi.fn().mockResolvedValue(mockCheckout); - await manager.executeOperation('confirm', anotherMockOperation); - - // Listener should not be called after unsubscribing - expect(listener).not.toHaveBeenCalled(); - }); - - it('should notify all listeners when state changes', async () => { - const listener1: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - const listener2: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - const mockCheckout = createMockCheckoutResource(); - - manager.subscribe(listener1); - manager.subscribe(listener2); - - const mockOperation = vi.fn().mockResolvedValue(mockCheckout); - await manager.executeOperation('start', mockOperation); - - expect(listener1).toHaveBeenCalled(); - expect(listener2).toHaveBeenCalled(); - - // Verify they were called with the updated state - const expectedState = expect.objectContaining({ - checkout: mockCheckout, - isStarting: false, - error: null, - fetchStatus: 'idle', - status: 'needs_confirmation', - }); - - expect(listener1).toHaveBeenCalledWith(expectedState); - expect(listener2).toHaveBeenCalledWith(expectedState); - }); - - it('should handle multiple subscribe/unsubscribe cycles', () => { - const listener: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - - // Subscribe and unsubscribe multiple times - const unsubscribe1 = manager.subscribe(listener); - unsubscribe1(); - - const unsubscribe2 = manager.subscribe(listener); - const unsubscribe3 = manager.subscribe(listener); - - unsubscribe2(); - unsubscribe3(); - - // Should not throw errors - expect(() => unsubscribe1()).not.toThrow(); - expect(() => unsubscribe2()).not.toThrow(); - }); - }); - - describe('executeOperation - start operations', () => { - it('should execute start operation successfully', async () => { - const mockCheckout = createMockCheckoutResource(); - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockResolvedValue(mockCheckout); - - const result = await manager.executeOperation('start', mockOperation); - - expect(mockOperation).toHaveBeenCalledOnce(); - expect(result).toEqual({ - data: mockCheckout, - error: null, - }); - - const finalState = manager.getCacheState(); - expect(finalState).toEqual( - expect.objectContaining({ - isStarting: false, - checkout: mockCheckout, - error: null, - fetchStatus: 'idle', - status: 'needs_confirmation', - }), - ); - }); - - it('should set isStarting to true during operation', async () => { - let capturedState: CheckoutCacheState | null = null; - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(async () => { - // Capture state while operation is running - capturedState = manager.getCacheState(); - return createMockCheckoutResource(); - }); - - await manager.executeOperation('start', mockOperation); - - expect(capturedState).toEqual( - expect.objectContaining({ - isStarting: true, - fetchStatus: 'fetching', - }), - ); - }); - - it('should handle operation errors correctly', async () => { - const mockError = createMockError('Operation failed'); - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockRejectedValue(mockError); - - const result = await manager.executeOperation('start', mockOperation); - - expect(result).toEqual({ - data: null, - error: mockError, - }); - - const finalState = manager.getCacheState(); - expect(finalState).toEqual( - expect.objectContaining({ - isStarting: false, - error: mockError, - fetchStatus: 'error', - status: 'needs_initialization', - }), - ); - }); - - it('should clear previous errors when starting new operation', async () => { - // First, create an error state - const mockError = createMockError('Previous error'); - const failingOperation: MockedFunction<() => Promise> = vi - .fn() - .mockRejectedValue(mockError); - - const result = await manager.executeOperation('start', failingOperation); - expect(result).toEqual({ - data: null, - error: mockError, - }); - - const errorState = manager.getCacheState(); - expect(errorState.error).toBe(mockError); - - // Now start a successful operation - const mockCheckout = createMockCheckoutResource(); - const successfulOperation: MockedFunction<() => Promise> = vi - .fn() - .mockResolvedValue(mockCheckout); - - const successResult = await manager.executeOperation('start', successfulOperation); - expect(successResult).toEqual({ - data: mockCheckout, - error: null, - }); - - const finalState = manager.getCacheState(); - expect(finalState.error).toBeNull(); - expect(finalState.checkout).toBe(mockCheckout); - }); - }); - - describe('executeOperation - confirm operations', () => { - it('should execute confirm operation successfully', async () => { - const mockCheckout = createMockCheckoutResource({ status: 'completed' }); - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockResolvedValue(mockCheckout); - - const result = await manager.executeOperation('confirm', mockOperation); - - expect(result).toEqual({ - data: mockCheckout, - error: null, - }); - - const finalState = manager.getCacheState(); - expect(finalState).toEqual( - expect.objectContaining({ - isConfirming: false, - checkout: mockCheckout, - error: null, - fetchStatus: 'idle', - status: 'completed', - }), - ); - }); - - it('should set isConfirming to true during operation', async () => { - let capturedState: CheckoutCacheState | null = null; - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(async () => { - capturedState = manager.getCacheState(); - return createMockCheckoutResource(); - }); - - await manager.executeOperation('confirm', mockOperation); - - expect(capturedState).toEqual( - expect.objectContaining({ - isConfirming: true, - fetchStatus: 'fetching', - }), - ); - }); - - it('should handle confirm operation errors', async () => { - const mockError = createMockError('Confirm failed'); - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockRejectedValue(mockError); - - const result = await manager.executeOperation('confirm', mockOperation); - expect(result).toEqual({ - data: null, - error: mockError, - }); - - const finalState = manager.getCacheState(); - expect(finalState).toEqual( - expect.objectContaining({ - isConfirming: false, - error: mockError, - fetchStatus: 'error', - }), - ); - }); - }); - - describe('operation deduplication', () => { - it('should deduplicate concurrent start operations', async () => { - const mockCheckout = createMockCheckoutResource(); - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(mockCheckout), 50))); - - // Start multiple operations concurrently - const [result1, result2, result3] = await Promise.all([ - manager.executeOperation('start', mockOperation), - manager.executeOperation('start', mockOperation), - manager.executeOperation('start', mockOperation), - ]); - - // Operation should only be called once - expect(mockOperation).toHaveBeenCalledOnce(); - - // All results should be the same - expect(result1).toEqual({ - data: mockCheckout, - error: null, - }); - expect(result2).toEqual({ - data: mockCheckout, - error: null, - }); - expect(result3).toEqual({ - data: mockCheckout, - error: null, - }); - }); - - it('should deduplicate concurrent confirm operations', async () => { - const mockCheckout = createMockCheckoutResource(); - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(mockCheckout), 50))); - - const [result1, result2] = await Promise.all([ - manager.executeOperation('confirm', mockOperation), - manager.executeOperation('confirm', mockOperation), - ]); - - expect(mockOperation).toHaveBeenCalledOnce(); - expect(result1).toBe(result2); - }); - - it('should allow different operation types to run concurrently', async () => { - const startCheckout = createMockCheckoutResource({ id: 'start_checkout' }); - const confirmCheckout = createMockCheckoutResource({ id: 'confirm_checkout' }); - - const startOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(startCheckout), 50))); - const confirmOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(confirmCheckout), 50))); - - const [startResult, confirmResult] = await Promise.all([ - manager.executeOperation('start', startOperation), - manager.executeOperation('confirm', confirmOperation), - ]); - - expect(startOperation).toHaveBeenCalledOnce(); - expect(confirmOperation).toHaveBeenCalledOnce(); - expect(startResult).toEqual({ - data: startCheckout, - error: null, - }); - expect(confirmResult).toEqual({ - data: confirmCheckout, - error: null, - }); - }); - - it('should propagate errors to all concurrent callers', async () => { - const mockError = createMockError('Concurrent operation failed'); - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(() => new Promise((_, reject) => setTimeout(() => reject(mockError), 50))); - - const promises = [ - manager.executeOperation('start', mockOperation), - manager.executeOperation('start', mockOperation), - manager.executeOperation('start', mockOperation), - ]; - - const results = await Promise.all(promises); - - // All promises should resolve with the same error - results.forEach(result => { - expect(result).toEqual({ - data: null, - error: mockError, - }); - }); - expect(mockOperation).toHaveBeenCalledOnce(); - }); - - it('should allow sequential operations of the same type', async () => { - const checkout1 = createMockCheckoutResource({ id: 'checkout1' }); - const checkout2 = createMockCheckoutResource({ id: 'checkout2' }); - - const operation1: MockedFunction<() => Promise> = vi.fn().mockResolvedValue(checkout1); - const operation2: MockedFunction<() => Promise> = vi.fn().mockResolvedValue(checkout2); - - const result1 = await manager.executeOperation('start', operation1); - const result2 = await manager.executeOperation('start', operation2); - - expect(operation1).toHaveBeenCalledOnce(); - expect(operation2).toHaveBeenCalledOnce(); - expect(result1).toEqual({ - data: checkout1, - error: null, - }); - expect(result2).toEqual({ - data: checkout2, - error: null, - }); - }); - }); - - describe('clearCheckout', () => { - it('should clear checkout state when no operations are pending', () => { - const listener: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - manager.subscribe(listener); - - manager.clearCheckout(); - - const state = manager.getCacheState(); - expect(state).toEqual({ - isStarting: false, - isConfirming: false, - error: null, - checkout: null, - fetchStatus: 'idle', - status: 'needs_initialization', - }); - - // Should notify listeners - expect(listener).toHaveBeenCalledWith(state); - }); - - it('should not clear checkout state when operations are pending', async () => { - const mockCheckout = createMockCheckoutResource(); - let resolveOperation: ((value: BillingCheckoutResource) => void) | undefined; - - const mockOperation: MockedFunction<() => Promise> = vi.fn().mockImplementation( - () => - new Promise(resolve => { - resolveOperation = resolve; - }), - ); - - // Start an operation but don't resolve it yet - const operationPromise = manager.executeOperation('start', mockOperation); - - // Verify operation is in progress - let state = manager.getCacheState(); - expect(state.isStarting).toBe(true); - expect(state.fetchStatus).toBe('fetching'); - - // Try to clear while operation is pending - manager.clearCheckout(); - - // State should not be cleared - state = manager.getCacheState(); - expect(state.isStarting).toBe(true); - expect(state.fetchStatus).toBe('fetching'); - - // Resolve the operation - resolveOperation?.(mockCheckout); - await operationPromise; - - // Now clearing should work - manager.clearCheckout(); - state = manager.getCacheState(); - expect(state.checkout).toBeNull(); - expect(state.status).toBe('needs_initialization'); - }); - }); - - describe('state derivation', () => { - it('should derive fetchStatus correctly based on operation state', async () => { - // Initially idle - expect(manager.getCacheState().fetchStatus).toBe(FETCH_STATUS.IDLE); - - // During operation - fetching - let capturedState: CheckoutCacheState | null = null; - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(async () => { - capturedState = manager.getCacheState(); - return createMockCheckoutResource(); - }); - - await manager.executeOperation('start', mockOperation); - expect(capturedState?.fetchStatus).toBe(FETCH_STATUS.FETCHING); - - // After successful operation - idle - expect(manager.getCacheState().fetchStatus).toBe(FETCH_STATUS.IDLE); - - // After error - error - const mockError = createMockError(); - const failingOperation: MockedFunction<() => Promise> = vi - .fn() - .mockRejectedValue(mockError); - - const result = await manager.executeOperation('start', failingOperation); - expect(result).toEqual({ - data: null, - error: mockError, - }); - expect(manager.getCacheState().fetchStatus).toBe(FETCH_STATUS.ERROR); - }); - - it('should derive status based on checkout state', async () => { - // Initially needs initialization - expect(manager.getCacheState().status).toBe('needs_initialization'); - - // After starting checkout - needs confirmation - const pendingCheckout = createMockCheckoutResource({ status: 'needs_confirmation' }); - const startOperation: MockedFunction<() => Promise> = vi - .fn() - .mockResolvedValue(pendingCheckout); - - await manager.executeOperation('start', startOperation); - expect(manager.getCacheState().status).toBe('needs_confirmation'); - - // After completing checkout - completed - const completedCheckout = createMockCheckoutResource({ status: 'completed' }); - const confirmOperation: MockedFunction<() => Promise> = vi - .fn() - .mockResolvedValue(completedCheckout); - - await manager.executeOperation('confirm', confirmOperation); - expect(manager.getCacheState().status).toBe('completed'); - }); - - it('should handle both operations running simultaneously', async () => { - let startCapturedState: CheckoutCacheState | null = null; - let confirmCapturedState: CheckoutCacheState | null = null; - - const startOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(async () => { - await new Promise(resolve => setTimeout(resolve, 30)); - startCapturedState = manager.getCacheState(); - return createMockCheckoutResource({ id: 'start' }); - }); - - const confirmOperation: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(async () => { - await new Promise(resolve => setTimeout(resolve, 20)); - confirmCapturedState = manager.getCacheState(); - return createMockCheckoutResource({ id: 'confirm' }); - }); - - await Promise.all([ - manager.executeOperation('start', startOperation), - manager.executeOperation('confirm', confirmOperation), - ]); - - // Both should have seen fetching status - expect(startCapturedState?.fetchStatus).toBe(FETCH_STATUS.FETCHING); - expect(confirmCapturedState?.fetchStatus).toBe(FETCH_STATUS.FETCHING); - - // At least one should have seen both operations running - expect( - (startCapturedState?.isStarting && startCapturedState?.isConfirming) || - (confirmCapturedState?.isStarting && confirmCapturedState?.isConfirming), - ).toBe(true); - }); - }); - - describe('cache isolation', () => { - it('should isolate state between different cache keys', async () => { - const manager1 = createCheckoutManager(createCacheKey('key1')); - const manager2 = createCheckoutManager(createCacheKey('key2')); - - const checkout1 = createMockCheckoutResource({ id: 'checkout1' }); - const checkout2 = createMockCheckoutResource({ id: 'checkout2' }); - - const operation1: MockedFunction<() => Promise> = vi.fn().mockResolvedValue(checkout1); - const operation2: MockedFunction<() => Promise> = vi.fn().mockResolvedValue(checkout2); - - await manager1.executeOperation('start', operation1); - await manager2.executeOperation('confirm', operation2); - - const state1 = manager1.getCacheState(); - const state2 = manager2.getCacheState(); - - expect(state1.checkout?.id).toBe('checkout1'); - expect(state1.status).toBe('needs_confirmation'); - - expect(state2.checkout?.id).toBe('checkout2'); - expect(state2.isStarting).toBe(false); - expect(state2.isConfirming).toBe(false); - }); - - it('should isolate listeners between different cache keys', async () => { - const manager1 = createCheckoutManager(createCacheKey('key1')); - const manager2 = createCheckoutManager(createCacheKey('key2')); - - const listener1: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - const listener2: MockedFunction<(state: CheckoutCacheState) => void> = vi.fn(); - - manager1.subscribe(listener1); - manager2.subscribe(listener2); - - // Trigger operation on manager1 - const mockOperation: MockedFunction<() => Promise> = vi - .fn() - .mockResolvedValue(createMockCheckoutResource()); - await manager1.executeOperation('start', mockOperation); - - // Only listener1 should be called - expect(listener1).toHaveBeenCalled(); - expect(listener2).not.toHaveBeenCalled(); - }); - - it('should isolate pending operations between different cache keys', async () => { - const manager1 = createCheckoutManager(createCacheKey('key1')); - const manager2 = createCheckoutManager(createCacheKey('key2')); - - const checkout1 = createMockCheckoutResource({ id: 'checkout1' }); - const checkout2 = createMockCheckoutResource({ id: 'checkout2' }); - - const operation1: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(checkout1), 50))); - const operation2: MockedFunction<() => Promise> = vi - .fn() - .mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(checkout2), 50))); - - // Start concurrent operations on both managers - const [result1, result2] = await Promise.all([ - manager1.executeOperation('start', operation1), - manager2.executeOperation('start', operation2), - ]); - - // Both operations should execute (not deduplicated across managers) - expect(operation1).toHaveBeenCalledOnce(); - expect(operation2).toHaveBeenCalledOnce(); - expect(result1).toEqual({ - data: checkout1, - error: null, - }); - expect(result2).toEqual({ - data: checkout2, - error: null, - }); - }); - }); -}); diff --git a/packages/clerk-js/src/core/modules/checkout/instance.ts b/packages/clerk-js/src/core/modules/checkout/instance.ts index 8b28b694f10..85224308462 100644 --- a/packages/clerk-js/src/core/modules/checkout/instance.ts +++ b/packages/clerk-js/src/core/modules/checkout/instance.ts @@ -1,12 +1,10 @@ -import type { - __experimental_CheckoutCacheState, - __experimental_CheckoutInstance, - __experimental_CheckoutOptions, - SetActiveNavigate, -} from '@clerk/shared/types'; +import type { __experimental_CheckoutOptions, CheckoutSignalValue } from '@clerk/shared/types'; + +import { CheckoutFlow, createSignals } from '@/core/resources/BillingCheckout'; import type { Clerk } from '../../clerk'; -import { type CheckoutKey, createCheckoutManager } from './manager'; + +type CheckoutKey = string & { readonly __tag: 'CheckoutKey' }; /** * Generate cache key for checkout instance @@ -16,72 +14,52 @@ function cacheKey(options: { userId: string; orgId?: string; planId: string; pla return `${userId}-${orgId || 'user'}-${planId}-${planPeriod}` as CheckoutKey; } +/** + * Stores the state of checkout instances based on their configuration as a cache key. + */ +const CheckoutSignalCache = new Map< + CheckoutKey, + { resource: CheckoutFlow; signals: ReturnType } +>(); + /** * Create a checkout instance with the given options */ -function createCheckoutInstance( - clerk: Clerk, - options: __experimental_CheckoutOptions, -): __experimental_CheckoutInstance { +function createCheckoutInstance(clerk: Clerk, options: __experimental_CheckoutOptions): CheckoutSignalValue { const { for: forOrganization, planId, planPeriod } = options; - if (!clerk.isSignedIn || !clerk.user) { + if (clerk.user === null) { throw new Error('Clerk: User is not authenticated'); } - if (forOrganization === 'organization' && !clerk.organization) { - throw new Error('Clerk: Use `setActive` to set the organization'); + if (forOrganization === 'organization' && clerk.organization === null) { + throw new Error( + 'Clerk: The current session does not have an active organization. Use `setActive` to set the organization', + ); } const checkoutKey = cacheKey({ - userId: clerk.user.id, + userId: clerk.user?.id || '', orgId: forOrganization === 'organization' ? clerk.organization?.id : undefined, planId, planPeriod, }); - const manager = createCheckoutManager(checkoutKey); - - const start: __experimental_CheckoutInstance['start'] = async () => { - return manager.executeOperation('start', async () => { - const result = await clerk.billing?.startCheckout({ - ...(forOrganization === 'organization' ? { orgId: clerk.organization?.id } : {}), - planId, - planPeriod, - }); - return result; - }); - }; - - const confirm: __experimental_CheckoutInstance['confirm'] = async params => { - return manager.executeOperation('confirm', async () => { - const checkout = manager.getCacheState().checkout; - if (!checkout) { - throw new Error('Clerk: Call `start` before `confirm`'); - } - return checkout.confirm(params); - }); - }; - - const finalize = (params?: { navigate?: SetActiveNavigate }) => { - const { navigate } = params || {}; - return clerk.setActive({ session: clerk.session?.id, navigate }); - }; + const checkoutInstance = CheckoutSignalCache.get(checkoutKey); + if (checkoutInstance) { + return checkoutInstance.signals.computedSignal() as CheckoutSignalValue; + } - const clear = () => manager.clearCheckout(); + const signals = createSignals(); - const subscribe = (listener: (state: __experimental_CheckoutCacheState) => void) => { - return manager.subscribe(listener); - }; + const checkout = new CheckoutFlow(signals, { + ...(forOrganization === 'organization' ? { orgId: clerk.organization?.id } : {}), + planId, + planPeriod, + }); - return { - start, - confirm, - finalize, - clear, - subscribe, - getState: manager.getCacheState, - }; + CheckoutSignalCache.set(checkoutKey, { resource: checkout, signals }); + return signals.computedSignal() as CheckoutSignalValue; } export { createCheckoutInstance }; diff --git a/packages/clerk-js/src/core/modules/checkout/manager.ts b/packages/clerk-js/src/core/modules/checkout/manager.ts deleted file mode 100644 index e0b5960038b..00000000000 --- a/packages/clerk-js/src/core/modules/checkout/manager.ts +++ /dev/null @@ -1,195 +0,0 @@ -import type { - __experimental_CheckoutCacheState, - __experimental_CheckoutInstance, - BillingCheckoutResource, - ClerkAPIResponseError, -} from '@clerk/shared/types'; - -type CheckoutKey = string & { readonly __tag: 'CheckoutKey' }; - -type CheckoutResult = Awaited>; - -const createManagerCache = () => { - const cache = new Map(); - const listeners = new Map void>>(); - const pendingOperations = new Map>>(); - - return { - cache, - listeners, - pendingOperations, - safeGet>(key: K, map: Map): NonNullable { - if (!map.has(key)) { - map.set(key, new Set() as V); - } - return map.get(key) as NonNullable; - }, - safeGetOperations(key: K): Map> { - if (!this.pendingOperations.has(key)) { - this.pendingOperations.set(key, new Map>()); - } - return this.pendingOperations.get(key) as Map>; - }, - }; -}; - -const managerCache = createManagerCache(); - -const CHECKOUT_STATUS = { - NEEDS_INITIALIZATION: 'needs_initialization', - NEEDS_CONFIRMATION: 'needs_confirmation', - COMPLETED: 'completed', -} as const; - -export const FETCH_STATUS = { - IDLE: 'idle', - FETCHING: 'fetching', - ERROR: 'error', -} as const; - -/** - * Derives the checkout state from the base state. - */ -function deriveCheckoutState( - baseState: Omit<__experimental_CheckoutCacheState, 'fetchStatus' | 'status'>, -): __experimental_CheckoutCacheState { - const fetchStatus = (() => { - if (baseState.isStarting || baseState.isConfirming) { - return FETCH_STATUS.FETCHING; - } - if (baseState.error) { - return FETCH_STATUS.ERROR; - } - return FETCH_STATUS.IDLE; - })(); - - const status = (() => { - if (baseState.checkout?.status === CHECKOUT_STATUS.COMPLETED) { - return CHECKOUT_STATUS.COMPLETED; - } - if (baseState.checkout) { - return CHECKOUT_STATUS.NEEDS_CONFIRMATION; - } - return CHECKOUT_STATUS.NEEDS_INITIALIZATION; - })(); - - return { - ...baseState, - fetchStatus, - status, - }; -} - -const defaultCacheState: __experimental_CheckoutCacheState = Object.freeze( - deriveCheckoutState({ - isStarting: false, - isConfirming: false, - error: null, - checkout: null, - }), -); - -/** - * Creates a checkout manager for handling checkout operations and state management. - * - * @param cacheKey - Unique identifier for the checkout instance - * @returns Manager with methods for checkout operations and state subscription - * - * @example - * ```typescript - * const manager = createCheckoutManager('user-123-plan-456-monthly'); - * const unsubscribe = manager.subscribe(state => console.log(state)); - * ``` - */ -function createCheckoutManager(cacheKey: CheckoutKey) { - const listeners = managerCache.safeGet(cacheKey, managerCache.listeners); - const pendingOperations = managerCache.safeGetOperations(cacheKey); - - const notifyListeners = () => { - listeners.forEach(listener => listener(getCacheState())); - }; - - const getCacheState = (): __experimental_CheckoutCacheState => { - return managerCache.cache.get(cacheKey) || defaultCacheState; - }; - - const updateCacheState = ( - updates: Partial>, - ): void => { - const currentState = getCacheState(); - const baseState = { ...currentState, ...updates }; - const newState = deriveCheckoutState(baseState); - managerCache.cache.set(cacheKey, Object.freeze(newState)); - notifyListeners(); - }; - - return { - subscribe(listener: (newState: __experimental_CheckoutCacheState) => void): () => void { - listeners.add(listener); - return () => { - listeners.delete(listener); - }; - }, - - getCacheState, - - // Shared operation handler to eliminate duplication - async executeOperation( - operationType: 'start' | 'confirm', - operationFn: () => Promise, - ): Promise { - const operationId = `${cacheKey}-${operationType}`; - const isRunningField = operationType === 'start' ? 'isStarting' : 'isConfirming'; - - // Check if there's already a pending operation - const existingOperation = pendingOperations.get(operationId); - if (existingOperation) { - // Wait for the existing operation to complete and return its result - // If it fails, all callers should receive the same error - return await existingOperation; - } - - // Create and store the operation promise - const operationPromise = (async () => { - let data: BillingCheckoutResource | null = null; - let error: ClerkAPIResponseError | null = null; - try { - // Mark operation as in progress and clear any previous errors - updateCacheState({ - [isRunningField]: true, - error: null, - ...(operationType === 'start' ? { checkout: null } : {}), - }); - - // Execute the checkout operation - const result = await operationFn(); - - // Update state with successful result - updateCacheState({ [isRunningField]: false, error: null, checkout: result }); - data = result; - } catch (e) { - // Cast error to expected type and update state - const clerkError = e as ClerkAPIResponseError; - error = clerkError; - updateCacheState({ [isRunningField]: false, error: clerkError }); - } finally { - // Always clean up pending operation tracker - pendingOperations.delete(operationId); - } - return { data, error } as CheckoutResult; - })(); - - pendingOperations.set(operationId, operationPromise); - return operationPromise; - }, - - clearCheckout(): void { - // Only reset the state if there are no pending operations - if (pendingOperations.size === 0) { - updateCacheState(defaultCacheState); - } - }, - }; -} - -export { createCheckoutManager, type __experimental_CheckoutCacheState as CheckoutCacheState, type CheckoutKey }; diff --git a/packages/clerk-js/src/core/resources/BillingCheckout.ts b/packages/clerk-js/src/core/resources/BillingCheckout.ts index e3229d3f0cd..0dbadd220fa 100644 --- a/packages/clerk-js/src/core/resources/BillingCheckout.ts +++ b/packages/clerk-js/src/core/resources/BillingCheckout.ts @@ -1,3 +1,4 @@ +import type { ClerkError } from '@clerk/shared/error'; import { isClerkAPIResponseError } from '@clerk/shared/error'; import { retry } from '@clerk/shared/retry'; import type { @@ -7,13 +8,20 @@ import type { BillingPayerResource, BillingPaymentMethodResource, BillingSubscriptionPlanPeriod, + CheckoutFlowFinalizeParams, + CheckoutFlowResource, + CheckoutFlowResourceNonStrict, + CheckoutSignalValue, ConfirmCheckoutParams, + CreateCheckoutParams, } from '@clerk/shared/types'; +import { computed, endBatch, signal, startBatch } from 'alien-signals'; import { unixEpochToDate } from '@/utils/date'; import { billingTotalsFromJSON } from '../../utils'; import { Billing } from '../modules/billing/namespace'; +import { errorsToParsedErrors } from '../signals'; import { BillingPayer } from './BillingPayer'; import { BaseResource, BillingPaymentMethod, BillingPlan } from './internal'; @@ -32,7 +40,7 @@ export class BillingCheckout extends BaseResource implements BillingCheckoutReso payer!: BillingPayerResource; needsPaymentMethod!: boolean; - constructor(data: BillingCheckoutJSON) { + constructor(data: BillingCheckoutJSON | null = null) { super(); this.fromJSON(data); } @@ -91,3 +99,163 @@ export class BillingCheckout extends BaseResource implements BillingCheckoutReso ); }; } + +export const createSignals = () => { + const resourceSignal = signal<{ resource: CheckoutFlow | null }>({ resource: null }); + const errorSignal = signal<{ error: ClerkError | null }>({ error: null }); + const fetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); + const computedSignal = computed & { checkout: CheckoutFlowResource | null }>( + () => { + const resource = resourceSignal().resource; + const error = errorSignal().error; + const fetchStatus = fetchSignal().status; + const errors = errorsToParsedErrors(error, {}); + return { errors: errors, fetchStatus, checkout: resource }; + }, + ); + + return { resourceSignal, errorSignal, fetchSignal, computedSignal }; +}; + +type CheckoutTask = 'start' | 'confirm' | 'finalize'; + +export class CheckoutFlow implements CheckoutFlowResourceNonStrict { + private resource = new BillingCheckout(null); + private readonly config: CreateCheckoutParams; + private readonly signals: ReturnType; + private readonly pendingOperations = new Map | null>(); + + constructor(signals: ReturnType, config: CreateCheckoutParams) { + this.config = config; + this.signals = signals; + this.signals.resourceSignal({ resource: this }); + } + + get status() { + return this.resource.status ?? 'needs_initialization'; + } + + get externalClientSecret() { + return this.resource.externalClientSecret; + } + + get externalGatewayId() { + return this.resource.externalGatewayId; + } + + get plan() { + return this.resource.plan; + } + get planPeriod() { + return this.resource.planPeriod; + } + get totals() { + return this.resource.totals; + } + get isImmediatePlanChange() { + return this.resource.isImmediatePlanChange; + } + get freeTrialEndsAt() { + return this.resource.freeTrialEndsAt; + } + get payer() { + return this.resource.payer; + } + + get paymentMethod() { + return this.resource.paymentMethod ?? null; + } + + get planPeriodStart() { + return this.resource.planPeriodStart; + } + + get needsPaymentMethod() { + return this.resource.needsPaymentMethod; + } + + async start(): Promise<{ error: ClerkError | null }> { + return this.runAsyncCheckoutTask( + 'start', + async () => { + const checkout = (await BillingCheckout.clerk.billing?.startCheckout(this.config)) as BillingCheckout; + this.resource = checkout; + }, + () => { + this.resource = new BillingCheckout(null); + this.signals.resourceSignal({ resource: this }); + }, + ); + } + + async confirm(params: ConfirmCheckoutParams): Promise<{ error: ClerkError | null }> { + if (!this.resource.id) { + throw new Error('Clerk: `start()` must be called before `confirm()`'); + } + return this.runAsyncCheckoutTask('confirm', async () => { + await this.resource.confirm(params); + }); + } + + async finalize(params?: CheckoutFlowFinalizeParams): Promise<{ error: ClerkError | null }> { + const { navigate } = params || {}; + return this.runAsyncCheckoutTask('finalize', async () => { + if (this.resource.status !== 'completed') { + throw new Error('Clerk: `confirm()` must be called before `finalize()`'); + } + + await BillingCheckout.clerk.setActive({ session: BillingCheckout.clerk.session?.id, navigate }); + }); + } + + private runAsyncCheckoutTask(operationType: CheckoutTask, task: () => Promise, beforeTask?: () => void) { + // Noops during transitive state + if (typeof BillingCheckout.clerk.user === 'undefined') { + console.warn('Clerk: Checkout operations cannot be performed during transitive state'); + return { error: null }; + } + return createRunAsyncCheckoutTask(this, this.signals, this.pendingOperations)(operationType, task, beforeTask); + } +} + +function createRunAsyncCheckoutTask( + resource: CheckoutFlow, + signals: ReturnType, + pendingOperations: Map | null>, +): ( + operationType: CheckoutTask, + task: () => Promise, + beforeTask?: () => void, +) => Promise<{ error: ClerkError | null }> { + return async (operationType, task, beforeTask?: () => void) => { + if (pendingOperations.get(operationType)) { + // Wait for the existing operation to complete and return its result + // If it fails, all callers should receive the same error + return pendingOperations.get(operationType) as Promise<{ error: unknown }>; + } + + const operationPromise = (async () => { + startBatch(); + signals.errorSignal({ error: null }); + signals.fetchSignal({ status: 'fetching' }); + beforeTask?.(); + endBatch(); + startBatch(); + try { + await task(); + signals.resourceSignal({ resource: resource }); + return { error: null }; + } catch (err) { + signals.errorSignal({ error: err }); + return { error: err }; + } finally { + pendingOperations.delete(operationType); + signals.fetchSignal({ status: 'idle' }); + endBatch(); + } + })(); + + pendingOperations.set(operationType, operationPromise); + return operationPromise; + }; +} diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index 3371b8435c0..f246d7efbad 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -1,5 +1,4 @@ import type { - ActiveSessionResource, ClientJSON, ClientJSONSnapshot, ClientResource, @@ -57,13 +56,6 @@ export class Client extends BaseResource implements ClientResource { return this.signIn; } - /** - * @deprecated Use `signedInSessions()` instead. - */ - get activeSessions(): ActiveSessionResource[] { - return this.sessions.filter(s => s.status === 'active') as ActiveSessionResource[]; - } - get signedInSessions(): SignedInSessionResource[] { return this.sessions.filter(s => s.status === 'active' || s.status === 'pending') as SignedInSessionResource[]; } diff --git a/packages/clerk-js/src/core/resources/DevTools.ts b/packages/clerk-js/src/core/resources/DevTools.ts deleted file mode 100644 index 9a517858604..00000000000 --- a/packages/clerk-js/src/core/resources/DevTools.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ClerkResourceJSON, DevToolsResource, EnableEnvironmentSettingParams } from '@clerk/shared/types'; - -import { BaseResource } from './Base'; - -/** - * @internal - */ -export class DevTools extends BaseResource implements DevToolsResource { - pathRoot = '/dev_tools'; - - protected fromJSON(_data: ClerkResourceJSON | null): this { - return this; - } - - async __internal_enableEnvironmentSetting(params: EnableEnvironmentSettingParams) { - await this._basePatch({ - path: `${this.pathRoot}/enable_environment_setting`, - body: params, - }); - } -} diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index 3ff481c5111..f9a47dbe2b7 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -33,7 +33,6 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource captchaWidgetType: CaptchaWidgetType = null; clerkJSVersion?: string; createOrganizationUrl: string = ''; - experimental__forceOauthFirst?: boolean; faviconImageUrl: string = ''; googleOneTapClientId?: string; homeUrl: string = ''; diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index f057787a747..5e961e3f354 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -2,6 +2,7 @@ import type { AuthConfigResource, CommerceSettingsResource, DisplayConfigResource, + EnableEnvironmentSettingParams, EnvironmentJSON, EnvironmentJSONSnapshot, EnvironmentResource, @@ -101,4 +102,11 @@ export class Environment extends BaseResource implements EnvironmentResource { protect_config: this.protectConfig.__internal_toSnapshot(), }; } + + async __internal_enableEnvironmentSetting(params: EnableEnvironmentSettingParams) { + await this._basePatch({ + path: `/dev_tools/enable_environment_setting`, + body: params, + }); + } } diff --git a/packages/clerk-js/src/core/resources/Passkey.ts b/packages/clerk-js/src/core/resources/Passkey.ts index ea415f635c5..f3325604f67 100644 --- a/packages/clerk-js/src/core/resources/Passkey.ts +++ b/packages/clerk-js/src/core/resources/Passkey.ts @@ -1,4 +1,8 @@ import { ClerkWebAuthnError } from '@clerk/shared/error'; +import { + serializePublicKeyCredential, + webAuthnCreateCredential as webAuthnCreateCredentialOnWindow, +} from '@clerk/shared/internal/clerk-js/passkeys'; import type { DeletedObjectJSON, DeletedObjectResource, @@ -15,10 +19,6 @@ import { } from '@clerk/shared/webauthn'; import { unixEpochToDate } from '../../utils/date'; -import { - serializePublicKeyCredential, - webAuthnCreateCredential as webAuthnCreateCredentialOnWindow, -} from '../../utils/passkeys'; import { clerkMissingWebAuthnPublicKeyOptions } from '../errors'; import { BaseResource, DeletedObject, PasskeyVerification } from './internal'; diff --git a/packages/clerk-js/src/core/resources/SamlAccount.ts b/packages/clerk-js/src/core/resources/SamlAccount.ts deleted file mode 100644 index 173ea50e848..00000000000 --- a/packages/clerk-js/src/core/resources/SamlAccount.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { - SamlAccountConnectionJSON, - SamlAccountConnectionJSONSnapshot, - SamlAccountConnectionResource, - SamlAccountJSON, - SamlAccountJSONSnapshot, - SamlAccountResource, - SamlIdpSlug, - VerificationResource, -} from '@clerk/shared/types'; - -import { unixEpochToDate } from '../../utils/date'; -import { BaseResource } from './Base'; -import { Verification } from './Verification'; - -export class SamlAccount extends BaseResource implements SamlAccountResource { - id!: string; - provider: SamlIdpSlug = 'saml_custom'; - providerUserId: string | null = null; - active = false; - emailAddress = ''; - firstName = ''; - lastName = ''; - verification: VerificationResource | null = null; - samlConnection: SamlAccountConnectionResource | null = null; - lastAuthenticatedAt: Date | null = null; - enterpriseConnectionId: string | null = null; - - public constructor(data: Partial, pathRoot: string); - public constructor(data: SamlAccountJSON | SamlAccountJSONSnapshot, pathRoot: string) { - super(); - this.pathRoot = pathRoot; - this.fromJSON(data); - } - - protected fromJSON(data: SamlAccountJSON | SamlAccountJSONSnapshot | null): this { - if (!data) { - return this; - } - - this.id = data.id; - this.provider = data.provider; - this.providerUserId = data.provider_user_id; - this.active = data.active; - this.emailAddress = data.email_address; - this.firstName = data.first_name; - this.lastName = data.last_name; - this.enterpriseConnectionId = data.enterprise_connection_id; - - if (data.verification) { - this.verification = new Verification(data.verification); - } - - if (data.saml_connection) { - this.samlConnection = new SamlAccountConnection(data.saml_connection); - } - - this.lastAuthenticatedAt = data.last_authenticated_at ? unixEpochToDate(data.last_authenticated_at) : null; - - return this; - } - - public __internal_toSnapshot(): SamlAccountJSONSnapshot { - return { - object: 'saml_account', - id: this.id, - provider: this.provider, - provider_user_id: this.providerUserId, - active: this.active, - email_address: this.emailAddress, - first_name: this.firstName, - last_name: this.lastName, - verification: this.verification?.__internal_toSnapshot() || null, - saml_connection: this.samlConnection?.__internal_toSnapshot(), - enterprise_connection_id: this.enterpriseConnectionId, - last_authenticated_at: this.lastAuthenticatedAt ? this.lastAuthenticatedAt.getTime() : null, - }; - } -} - -export class SamlAccountConnection extends BaseResource implements SamlAccountConnectionResource { - id!: string; - name!: string; - domain!: string; - active!: boolean; - provider!: string; - syncUserAttributes!: boolean; - allowSubdomains!: boolean; - allowIdpInitiated!: boolean; - disableAdditionalIdentifications!: boolean; - createdAt!: Date; - updatedAt!: Date; - - constructor(data: SamlAccountConnectionJSON | SamlAccountConnectionJSONSnapshot | null) { - super(); - this.fromJSON(data); - } - protected fromJSON(data: SamlAccountConnectionJSON | SamlAccountConnectionJSONSnapshot | null): this { - if (data) { - this.id = data.id; - this.name = data.name; - this.domain = data.domain; - this.active = data.active; - this.provider = data.provider; - this.syncUserAttributes = data.sync_user_attributes; - this.allowSubdomains = data.allow_subdomains; - this.allowIdpInitiated = data.allow_idp_initiated; - this.disableAdditionalIdentifications = data.disable_additional_identifications; - this.createdAt = unixEpochToDate(data.created_at); - this.updatedAt = unixEpochToDate(data.updated_at); - } - - return this; - } - - public __internal_toSnapshot(): SamlAccountConnectionJSONSnapshot { - return { - object: 'saml_account_connection', - id: this.id, - name: this.name, - domain: this.domain, - active: this.active, - provider: this.provider, - sync_user_attributes: this.syncUserAttributes, - allow_subdomains: this.allowSubdomains, - allow_idp_initiated: this.allowIdpInitiated, - disable_additional_identifications: this.disableAdditionalIdentifications, - created_at: this.createdAt.getTime(), - updated_at: this.updatedAt.getTime(), - }; - } -} diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index ee5df2c43a1..d49aefd2186 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -1,5 +1,10 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; import { ClerkWebAuthnError, is4xxError } from '@clerk/shared/error'; +import { + convertJSONToPublicKeyRequestOptions, + serializePublicKeyCredentialAssertion, + webAuthnGetCredential as webAuthnGetCredentialOnWindow, +} from '@clerk/shared/internal/clerk-js/passkeys'; import { retry } from '@clerk/shared/retry'; import type { ActClaim, @@ -28,11 +33,6 @@ import { isWebAuthnSupported as isWebAuthnSupportedOnWindow } from '@clerk/share import { unixEpochToDate } from '@/utils/date'; import { debugLogger } from '@/utils/debug'; -import { - convertJSONToPublicKeyRequestOptions, - serializePublicKeyCredentialAssertion, - webAuthnGetCredential as webAuthnGetCredentialOnWindow, -} from '@/utils/passkeys'; import { TokenId } from '@/utils/tokenId'; import { clerkInvalidStrategy, clerkMissingWebAuthnPublicKeyOptions } from '../errors'; diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 73f43de78e6..b6d0bde5823 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -1,5 +1,13 @@ import { inBrowser } from '@clerk/shared/browser'; import { type ClerkError, ClerkRuntimeError, ClerkWebAuthnError } from '@clerk/shared/error'; +import { + convertJSONToPublicKeyRequestOptions, + serializePublicKeyCredentialAssertion, + webAuthnGetCredential as webAuthnGetCredentialOnWindow, +} from '@clerk/shared/internal/clerk-js/passkeys'; +import { createValidatePassword } from '@clerk/shared/internal/clerk-js/passwords/password'; +import { getClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams'; +import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate'; import { Poller } from '@clerk/shared/poller'; import type { AttemptFirstFactorParams, @@ -24,7 +32,6 @@ import type { ResetPasswordEmailCodeFactorConfig, ResetPasswordParams, ResetPasswordPhoneCodeFactorConfig, - SamlConfig, SignInCreateParams, SignInFirstFactor, SignInFutureBackupCodeVerifyParams, @@ -64,31 +71,14 @@ import { import { debugLogger } from '@/utils/debug'; -import { - generateSignatureWithBase, - generateSignatureWithCoinbaseWallet, - generateSignatureWithMetamask, - generateSignatureWithOKXWallet, - getBaseIdentifier, - getBrowserLocale, - getClerkQueryParam, - getCoinbaseWalletIdentifier, - getMetamaskIdentifier, - getOKXWalletIdentifier, - windowNavigate, -} from '../../utils'; +import { getBrowserLocale, web3 } from '../../utils'; import { _authenticateWithPopup, _futureAuthenticateWithPopup, wrapWithPopupRoutes, } from '../../utils/authenticateWithPopup'; -import { - convertJSONToPublicKeyRequestOptions, - serializePublicKeyCredentialAssertion, - webAuthnGetCredential as webAuthnGetCredentialOnWindow, -} from '../../utils/passkeys'; -import { createValidatePassword } from '../../utils/passwords/password'; import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask'; +import { loadZxcvbn } from '../../utils/zxcvbn'; import { clerkInvalidFAPIResponse, clerkInvalidStrategy, @@ -220,12 +210,6 @@ export class SignIn extends BaseResource implements SignInResource { case 'reset_password_email_code': config = { emailAddressId: params.emailAddressId } as ResetPasswordEmailCodeFactorConfig; break; - case 'saml': - config = { - redirectUrl: params.redirectUrl, - actionCompleteRedirectUrl: params.actionCompleteRedirectUrl, - } as SamlConfig; - break; case 'enterprise_sso': config = { redirectUrl: params.redirectUrl, @@ -345,7 +329,7 @@ export class SignIn extends BaseResource implements SignInResource { }); } - if (strategy === 'saml' || strategy === 'enterprise_sso') { + if (strategy === 'enterprise_sso') { await this.prepareFirstFactor({ strategy, redirectUrl, @@ -425,37 +409,37 @@ export class SignIn extends BaseResource implements SignInResource { }; public authenticateWithMetamask = async (): Promise => { - const identifier = await getMetamaskIdentifier(); + const identifier = await web3().getMetamaskIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithMetamask, + generateSignature: web3().generateSignatureWithMetamask, strategy: 'web3_metamask_signature', }); }; public authenticateWithCoinbaseWallet = async (): Promise => { - const identifier = await getCoinbaseWalletIdentifier(); + const identifier = await web3().getCoinbaseWalletIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithCoinbaseWallet, + generateSignature: web3().generateSignatureWithCoinbaseWallet, strategy: 'web3_coinbase_wallet_signature', }); }; public authenticateWithBase = async (): Promise => { - const identifier = await getBaseIdentifier(); + const identifier = await web3().getBaseIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithBase, + generateSignature: web3().generateSignatureWithBase, strategy: 'web3_base_signature', }); }; public authenticateWithOKXWallet = async (): Promise => { - const identifier = await getOKXWalletIdentifier(); + const identifier = await web3().getOKXWalletIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithOKXWallet, + generateSignature: web3().generateSignatureWithOKXWallet, strategy: 'web3_okx_wallet_signature', }); }; @@ -530,9 +514,9 @@ export class SignIn extends BaseResource implements SignInResource { }; validatePassword: ReturnType = (password, cb) => { - if (SignIn.clerk.__unstable__environment?.userSettings.passwordSettings) { - return createValidatePassword({ - ...SignIn.clerk.__unstable__environment?.userSettings.passwordSettings, + if (SignIn.clerk.__internal_environment?.userSettings.passwordSettings) { + return createValidatePassword(loadZxcvbn(), { + ...SignIn.clerk.__internal_environment?.userSettings.passwordSettings, validatePassword: true, })(password, cb); } @@ -631,6 +615,7 @@ class SignInFuture implements SignInFutureResource { verifyBackupCode: this.verifyBackupCode.bind(this), }; + #hasBeenFinalized = false; readonly #resource: SignIn; constructor(resource: SignIn) { @@ -690,6 +675,10 @@ class SignInFuture implements SignInFutureResource { return this.#resource.secondFactorVerification; } + get hasBeenFinalized() { + return this.#hasBeenFinalized; + } + async sendResetPasswordEmailCode(): Promise<{ error: ClerkError | null }> { if (!this.#resource.id) { throw new Error('Cannot reset password without a sign in.'); @@ -975,20 +964,20 @@ class SignInFuture implements SignInFutureResource { let generateSignature; switch (provider) { case 'metamask': - identifier = await getMetamaskIdentifier(); - generateSignature = generateSignatureWithMetamask; + identifier = await web3().getMetamaskIdentifier(); + generateSignature = web3().generateSignatureWithMetamask; break; case 'coinbase_wallet': - identifier = await getCoinbaseWalletIdentifier(); - generateSignature = generateSignatureWithCoinbaseWallet; + identifier = await web3().getCoinbaseWalletIdentifier(); + generateSignature = web3().generateSignatureWithCoinbaseWallet; break; case 'base': - identifier = await getBaseIdentifier(); - generateSignature = generateSignatureWithBase; + identifier = await web3().getBaseIdentifier(); + generateSignature = web3().generateSignatureWithBase; break; case 'okx_wallet': - identifier = await getOKXWalletIdentifier(); - generateSignature = generateSignatureWithOKXWallet; + identifier = await web3().getOKXWalletIdentifier(); + generateSignature = web3().generateSignatureWithOKXWallet; break; default: throw new Error(`Unsupported Web3 provider: ${provider}`); @@ -1167,9 +1156,13 @@ class SignInFuture implements SignInFutureResource { } return runAsyncResourceTask(this.#resource, async () => { - // Reload the client to prevent an issue where the created session is not picked up. - await SignIn.clerk.client?.reload(); + // Reload the client if the created session is not in the client's sessions. This can happen during modal SSO + // flows where the in-memory client does not have the created session. + if (SignIn.clerk.client && !SignIn.clerk.client.sessions.some(s => s.id === this.#resource.createdSessionId)) { + await SignIn.clerk.client.reload(); + } + this.#hasBeenFinalized = true; await SignIn.clerk.setActive({ session: this.#resource.createdSessionId, navigate }); }); } diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index f234ded2ddd..d5403cd23f3 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -1,4 +1,6 @@ import { type ClerkError, ClerkRuntimeError, isCaptchaError, isClerkAPIResponseError } from '@clerk/shared/error'; +import { createValidatePassword } from '@clerk/shared/internal/clerk-js/passwords/password'; +import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate'; import { Poller } from '@clerk/shared/poller'; import type { AttemptEmailAddressVerificationParams, @@ -42,28 +44,16 @@ import type { import { debugLogger } from '@/utils/debug'; -import { - generateSignatureWithBase, - generateSignatureWithCoinbaseWallet, - generateSignatureWithMetamask, - generateSignatureWithOKXWallet, - getBaseIdentifier, - getBrowserLocale, - getClerkQueryParam, - getCoinbaseWalletIdentifier, - getMetamaskIdentifier, - getOKXWalletIdentifier, - windowNavigate, -} from '../../utils'; +import { getBrowserLocale, getClerkQueryParam, web3 } from '../../utils'; import { _authenticateWithPopup, _futureAuthenticateWithPopup, wrapWithPopupRoutes, } from '../../utils/authenticateWithPopup'; import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge'; -import { createValidatePassword } from '../../utils/passwords/password'; import { normalizeUnsafeMetadata } from '../../utils/resourceParams'; import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask'; +import { loadZxcvbn } from '../../utils/zxcvbn'; import { clerkInvalidFAPIResponse, clerkMissingOptionError, @@ -321,10 +311,10 @@ export class SignUp extends BaseResource implements SignUpResource { legalAccepted?: boolean; }, ): Promise => { - const identifier = await getMetamaskIdentifier(); + const identifier = await web3().getMetamaskIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithMetamask, + generateSignature: web3().generateSignatureWithMetamask, unsafeMetadata: params?.unsafeMetadata, strategy: 'web3_metamask_signature', legalAccepted: params?.legalAccepted, @@ -336,10 +326,10 @@ export class SignUp extends BaseResource implements SignUpResource { legalAccepted?: boolean; }, ): Promise => { - const identifier = await getCoinbaseWalletIdentifier(); + const identifier = await web3().getCoinbaseWalletIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithCoinbaseWallet, + generateSignature: web3().generateSignatureWithCoinbaseWallet, unsafeMetadata: params?.unsafeMetadata, strategy: 'web3_coinbase_wallet_signature', legalAccepted: params?.legalAccepted, @@ -351,10 +341,10 @@ export class SignUp extends BaseResource implements SignUpResource { legalAccepted?: boolean; }, ): Promise => { - const identifier = await getBaseIdentifier(); + const identifier = await web3().getBaseIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithBase, + generateSignature: web3().generateSignatureWithBase, unsafeMetadata: params?.unsafeMetadata, strategy: 'web3_base_signature', legalAccepted: params?.legalAccepted, @@ -366,10 +356,10 @@ export class SignUp extends BaseResource implements SignUpResource { legalAccepted?: boolean; }, ): Promise => { - const identifier = await getOKXWalletIdentifier(); + const identifier = await web3().getOKXWalletIdentifier(); return this.authenticateWithWeb3({ identifier, - generateSignature: generateSignatureWithOKXWallet, + generateSignature: web3().generateSignatureWithOKXWallet, unsafeMetadata: params?.unsafeMetadata, strategy: 'web3_okx_wallet_signature', legalAccepted: params?.legalAccepted, @@ -416,7 +406,7 @@ export class SignUp extends BaseResource implements SignUpResource { // If this fails again, we will let the caller handle the error accordingly. if (isClerkAPIResponseError(e) && isCaptchaError(e)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await SignUp.clerk.__unstable__environment!.reload(); + await SignUp.clerk.__internal_environment!.reload(); return authenticateFn(); } throw e; @@ -466,9 +456,9 @@ export class SignUp extends BaseResource implements SignUpResource { }; validatePassword: ReturnType = (password, cb) => { - if (SignUp.clerk.__unstable__environment?.userSettings.passwordSettings) { - return createValidatePassword({ - ...SignUp.clerk.__unstable__environment?.userSettings.passwordSettings, + if (SignUp.clerk.__internal_environment?.userSettings.passwordSettings) { + return createValidatePassword(loadZxcvbn(), { + ...SignUp.clerk.__internal_environment?.userSettings.passwordSettings, validatePassword: true, })(password, cb); } @@ -543,7 +533,7 @@ export class SignUp extends BaseResource implements SignUpResource { } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const captchaOauthBypass = SignUp.clerk.__unstable__environment!.displayConfig.captchaOauthBypass; + const captchaOauthBypass = SignUp.clerk.__internal_environment!.displayConfig.captchaOauthBypass; if (captchaOauthBypass.some(strategy => strategy === params.strategy)) { return true; @@ -580,6 +570,7 @@ class SignUpFuture implements SignUpFutureResource { verifyPhoneCode: this.verifyPhoneCode.bind(this), }; + #hasBeenFinalized = false; readonly #resource: SignUp; constructor(resource: SignUp) { @@ -684,6 +675,10 @@ class SignUpFuture implements SignUpFutureResource { return undefined; } + get hasBeenFinalized() { + return this.#hasBeenFinalized; + } + private async getCaptchaToken(): Promise<{ captchaToken?: string; captchaWidgetType?: CaptchaWidgetType; @@ -852,7 +847,7 @@ class SignUpFuture implements SignUpFutureResource { await authenticateFn().catch(async e => { if (isClerkAPIResponseError(e) && isCaptchaError(e)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await SignUp.clerk.__unstable__environment!.reload(); + await SignUp.clerk.__internal_environment!.reload(); return authenticateFn(); } throw e; @@ -881,20 +876,20 @@ class SignUpFuture implements SignUpFutureResource { let generateSignature; switch (provider) { case 'metamask': - identifier = await getMetamaskIdentifier(); - generateSignature = generateSignatureWithMetamask; + identifier = await web3().getMetamaskIdentifier(); + generateSignature = web3().generateSignatureWithMetamask; break; case 'coinbase_wallet': - identifier = await getCoinbaseWalletIdentifier(); - generateSignature = generateSignatureWithCoinbaseWallet; + identifier = await web3().getCoinbaseWalletIdentifier(); + generateSignature = web3().generateSignatureWithCoinbaseWallet; break; case 'base': - identifier = await getBaseIdentifier(); - generateSignature = generateSignatureWithBase; + identifier = await web3().getBaseIdentifier(); + generateSignature = web3().generateSignatureWithBase; break; case 'okx_wallet': - identifier = await getOKXWalletIdentifier(); - generateSignature = generateSignatureWithOKXWallet; + identifier = await web3().getOKXWalletIdentifier(); + generateSignature = web3().generateSignatureWithOKXWallet; break; default: throw new Error(`Unsupported Web3 provider: ${provider}`); @@ -950,6 +945,7 @@ class SignUpFuture implements SignUpFutureResource { throw new Error('Cannot finalize sign-up without a created session.'); } + this.#hasBeenFinalized = true; await SignUp.clerk.setActive({ session: this.#resource.createdSessionId, navigate }); }); } diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index 8f61851b1c5..ae7ef203c63 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -1,3 +1,4 @@ +import { getFullName } from '@clerk/shared/internal/clerk-js/user'; import type { BackupCodeJSON, BackupCodeResource, @@ -19,7 +20,6 @@ import type { PasskeyResource, PhoneNumberResource, RemoveUserPasswordParams, - SamlAccountResource, SetProfileImageParams, TOTPJSON, TOTPResource, @@ -34,7 +34,6 @@ import type { import { unixEpochToDate } from '../../utils/date'; import { normalizeUnsafeMetadata } from '../../utils/resourceParams'; -import { getFullName } from '../../utils/user'; import { eventBus, events } from '../events'; import { addPaymentMethod, getPaymentMethods, initializePaymentMethod } from '../modules/billing'; import { BackupCode } from './BackupCode'; @@ -49,7 +48,6 @@ import { OrganizationSuggestion, Passkey, PhoneNumber, - SamlAccount, SessionWithActivities, TOTP, UserOrganizationInvitation, @@ -68,9 +66,6 @@ export class User extends BaseResource implements UserResource { externalAccounts: ExternalAccountResource[] = []; enterpriseAccounts: EnterpriseAccountResource[] = []; passkeys: PasskeyResource[] = []; - - samlAccounts: SamlAccountResource[] = []; - organizationMemberships: OrganizationMembershipResource[] = []; passwordEnabled = false; firstName: string | null = null; @@ -365,8 +360,6 @@ export class User extends BaseResource implements UserResource { this.organizationMemberships = (data.organization_memberships || []).map(om => new OrganizationMembership(om)); - this.samlAccounts = (data.saml_accounts || []).map(sa => new SamlAccount(sa, this.path() + '/saml_accounts')); - this.enterpriseAccounts = (data.enterprise_accounts || []).map( ea => new EnterpriseAccount(ea, this.path() + '/enterprise_accounts'), ); @@ -413,7 +406,6 @@ export class User extends BaseResource implements UserResource { external_accounts: this.externalAccounts.map(ea => ea.__internal_toSnapshot()), passkeys: this.passkeys.map(passkey => passkey.__internal_toSnapshot()), organization_memberships: this.organizationMemberships.map(om => om.__internal_toSnapshot()), - saml_accounts: this.samlAccounts.map(sa => sa.__internal_toSnapshot()), enterprise_accounts: this.enterpriseAccounts.map(ea => ea.__internal_toSnapshot()), totp_enabled: this.totpEnabled, backup_code_enabled: this.backupCodeEnabled, diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 8af85ab38c8..5e1ae2b84ac 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -6,7 +6,6 @@ import type { PasskeySettingsData, PasswordSettingsData, PhoneCodeChannel, - SamlSettings, SignInData, SignUpData, UsernameSettingsData, @@ -112,9 +111,6 @@ export class UserSettings extends BaseResource implements UserSettingsResource { show_sign_in_button: false, }; passwordSettings: PasswordSettingsData = {} as PasswordSettingsData; - saml: SamlSettings = { - enabled: false, - }; signIn: SignInData = { second_factor: { required: false, @@ -229,7 +225,6 @@ export class UserSettings extends BaseResource implements UserSettingsResource { : Math.min(data.password_settings?.max_length ?? defaultMaxPasswordLength, defaultMaxPasswordLength), } : this.passwordSettings; - this.saml = this.withDefault(data.saml, this.saml); this.signIn = this.withDefault(data.sign_in, this.signIn); this.signUp = this.withDefault(data.sign_up, this.signUp); this.social = this.withDefault(data.social, this.social); @@ -256,7 +251,6 @@ export class UserSettings extends BaseResource implements UserSettingsResource { attributes: this.attributes, passkey_settings: this.passkeySettings, password_settings: this.passwordSettings, - saml: this.saml, sign_in: this.signIn, sign_up: this.signUp, social: this.social, diff --git a/packages/clerk-js/src/core/resources/Verification.ts b/packages/clerk-js/src/core/resources/Verification.ts index d9f739d2565..af1f61a4f88 100644 --- a/packages/clerk-js/src/core/resources/Verification.ts +++ b/packages/clerk-js/src/core/resources/Verification.ts @@ -1,4 +1,5 @@ import { ClerkAPIError, errorToJSON } from '@clerk/shared/error'; +import { convertJSONToPublicKeyCreateOptions } from '@clerk/shared/internal/clerk-js/passkeys'; import type { PasskeyVerificationResource, PhoneCodeChannel, @@ -17,7 +18,6 @@ import type { } from '@clerk/shared/types'; import { unixEpochToDate } from '../../utils/date'; -import { convertJSONToPublicKeyCreateOptions } from '../../utils/passkeys'; import { BaseResource } from './internal'; export class Verification extends BaseResource implements VerificationResource { diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 8460a87ce49..b89fea63ac6 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -1302,11 +1302,11 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getMetamaskIdentifierModule = await import('../../../utils'); - vi.spyOn(getMetamaskIdentifierModule, 'getMetamaskIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getMetamaskIdentifierModule, 'generateSignatureWithMetamask').mockResolvedValue('signature_123'); + const utilsModule = await import('../../../utils'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getMetamaskIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithMetamask: vi.fn().mockResolvedValue('signature_123'), + } as any); const signIn = new SignIn(); await signIn.__internal_future.web3({ strategy: 'web3_metamask_signature' }); @@ -1360,13 +1360,11 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getCoinbaseWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'getCoinbaseWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'generateSignatureWithCoinbaseWallet').mockResolvedValue( - 'signature_123', - ); + const utilsModule = await import('../../../utils'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getCoinbaseWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithCoinbaseWallet: vi.fn().mockResolvedValue('signature_123'), + } as any); const signIn = new SignIn(); await signIn.__internal_future.web3({ strategy: 'web3_coinbase_wallet_signature' }); @@ -1405,19 +1403,16 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getCoinbaseWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'getCoinbaseWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - + const utilsModule = await import('../../../utils'); const mockGenerateSignature = vi .fn() .mockRejectedValueOnce({ code: 4001, message: 'User rejected' }) .mockResolvedValueOnce('signature_123'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'generateSignatureWithCoinbaseWallet').mockImplementation( - mockGenerateSignature, - ); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getCoinbaseWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithCoinbaseWallet: mockGenerateSignature, + } as any); const signIn = new SignIn(); await signIn.__internal_future.web3({ strategy: 'web3_coinbase_wallet_signature' }); @@ -1453,13 +1448,12 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getCoinbaseWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'getCoinbaseWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - + const utilsModule = await import('../../../utils'); const mockError = { code: 5000, message: 'Other error' }; - vi.spyOn(getCoinbaseWalletIdentifierModule, 'generateSignatureWithCoinbaseWallet').mockRejectedValue(mockError); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getCoinbaseWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithCoinbaseWallet: vi.fn().mockRejectedValue(mockError), + } as any); const signIn = new SignIn(); const result = await signIn.__internal_future.web3({ strategy: 'web3_coinbase_wallet_signature' }); @@ -1491,11 +1485,11 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getBaseIdentifierModule = await import('../../../utils'); - vi.spyOn(getBaseIdentifierModule, 'getBaseIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getBaseIdentifierModule, 'generateSignatureWithBase').mockResolvedValue('signature_123'); + const utilsModule = await import('../../../utils'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getBaseIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithBase: vi.fn().mockResolvedValue('signature_123'), + } as any); const signIn = new SignIn(); await signIn.__internal_future.web3({ strategy: 'web3_base_signature' }); @@ -1534,11 +1528,11 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getOKXWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getOKXWalletIdentifierModule, 'getOKXWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getOKXWalletIdentifierModule, 'generateSignatureWithOKXWallet').mockResolvedValue('signature_123'); + const utilsModule = await import('../../../utils'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getOKXWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithOKXWallet: vi.fn().mockResolvedValue('signature_123'), + } as any); const signIn = new SignIn(); await signIn.__internal_future.web3({ strategy: 'web3_okx_wallet_signature' }); @@ -1564,10 +1558,10 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getMetamaskIdentifierModule = await import('../../../utils'); - vi.spyOn(getMetamaskIdentifierModule, 'getMetamaskIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); + const utilsModule = await import('../../../utils'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getMetamaskIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + } as any); const signIn = new SignIn(); const result = await signIn.__internal_future.web3({ strategy: 'web3_metamask_signature' }); @@ -1596,10 +1590,10 @@ describe('SignIn', () => { }); BaseResource._fetch = mockFetch; - const getMetamaskIdentifierModule = await import('../../../utils'); - vi.spyOn(getMetamaskIdentifierModule, 'getMetamaskIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); + const utilsModule = await import('../../../utils'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getMetamaskIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + } as any); const signIn = new SignIn(); const result = await signIn.__internal_future.web3({ strategy: 'web3_metamask_signature' }); diff --git a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts index d6771c120c4..663fa63d6d2 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts @@ -283,7 +283,7 @@ describe('SignUp', () => { buildUrlWithAuth: mockBuildUrlWithAuth, buildUrl: vi.fn().mockImplementation(path => 'https://example.com' + path), frontendApi: 'clerk.example.com', - __unstable__environment: { + __internal_environment: { reload: vi.fn().mockResolvedValue({}), }, } as any; @@ -374,17 +374,18 @@ describe('SignUp', () => { }); BaseResource._fetch = mockFetch; - const getMetamaskIdentifierModule = await import('../../../utils'); - vi.spyOn(getMetamaskIdentifierModule, 'getMetamaskIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getMetamaskIdentifierModule, 'generateSignatureWithMetamask').mockResolvedValue('signature_123'); + const utilsModule = await import('../../../utils'); + const mockGenerateSignature = vi.fn().mockResolvedValue('signature_123'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getMetamaskIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithMetamask: mockGenerateSignature, + } as any); const signUp = new SignUp(); await signUp.__internal_future.web3({ strategy: 'web3_metamask_signature' }); // Verify signature generation was called - expect(getMetamaskIdentifierModule.generateSignatureWithMetamask).toHaveBeenCalled(); + expect(mockGenerateSignature).toHaveBeenCalled(); }); it('authenticates with coinbase_wallet strategy', async () => { @@ -414,19 +415,18 @@ describe('SignUp', () => { }); BaseResource._fetch = mockFetch; - const getCoinbaseWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'getCoinbaseWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'generateSignatureWithCoinbaseWallet').mockResolvedValue( - 'signature_123', - ); + const utilsModule = await import('../../../utils'); + const mockGenerateSignature = vi.fn().mockResolvedValue('signature_123'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getCoinbaseWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithCoinbaseWallet: mockGenerateSignature, + } as any); const signUp = new SignUp(); await signUp.__internal_future.web3({ strategy: 'web3_coinbase_wallet_signature' }); // Verify signature generation was called - expect(getCoinbaseWalletIdentifierModule.generateSignatureWithCoinbaseWallet).toHaveBeenCalled(); + expect(mockGenerateSignature).toHaveBeenCalled(); }); it('authenticates with base strategy', async () => { @@ -456,17 +456,18 @@ describe('SignUp', () => { }); BaseResource._fetch = mockFetch; - const getBaseIdentifierModule = await import('../../../utils'); - vi.spyOn(getBaseIdentifierModule, 'getBaseIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getBaseIdentifierModule, 'generateSignatureWithBase').mockResolvedValue('signature_123'); + const utilsModule = await import('../../../utils'); + const mockGenerateSignature = vi.fn().mockResolvedValue('signature_123'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getBaseIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithBase: mockGenerateSignature, + } as any); const signUp = new SignUp(); await signUp.__internal_future.web3({ strategy: 'web3_base_signature' }); // Verify signature generation was called - expect(getBaseIdentifierModule.generateSignatureWithBase).toHaveBeenCalled(); + expect(mockGenerateSignature).toHaveBeenCalled(); }); it('authenticates with okx_wallet strategy', async () => { @@ -496,17 +497,18 @@ describe('SignUp', () => { }); BaseResource._fetch = mockFetch; - const getOKXWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getOKXWalletIdentifierModule, 'getOKXWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - vi.spyOn(getOKXWalletIdentifierModule, 'generateSignatureWithOKXWallet').mockResolvedValue('signature_123'); + const utilsModule = await import('../../../utils'); + const mockGenerateSignature = vi.fn().mockResolvedValue('signature_123'); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getOKXWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithOKXWallet: mockGenerateSignature, + } as any); const signUp = new SignUp(); await signUp.__internal_future.web3({ strategy: 'web3_okx_wallet_signature' }); // Verify signature generation was called - expect(getOKXWalletIdentifierModule.generateSignatureWithOKXWallet).toHaveBeenCalled(); + expect(mockGenerateSignature).toHaveBeenCalled(); }); it('retries coinbase_wallet signature on error code 4001', async () => { @@ -536,19 +538,16 @@ describe('SignUp', () => { }); BaseResource._fetch = mockFetch; - const getCoinbaseWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'getCoinbaseWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - + const utilsModule = await import('../../../utils'); const mockGenerateSignature = vi .fn() .mockRejectedValueOnce({ code: 4001, message: 'User rejected' }) .mockResolvedValueOnce('signature_123'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'generateSignatureWithCoinbaseWallet').mockImplementation( - mockGenerateSignature, - ); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getCoinbaseWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithCoinbaseWallet: mockGenerateSignature, + } as any); const signUp = new SignUp(); await signUp.__internal_future.web3({ strategy: 'web3_coinbase_wallet_signature' }); @@ -580,13 +579,12 @@ describe('SignUp', () => { }); BaseResource._fetch = mockFetch; - const getCoinbaseWalletIdentifierModule = await import('../../../utils'); - vi.spyOn(getCoinbaseWalletIdentifierModule, 'getCoinbaseWalletIdentifier').mockResolvedValue( - '0x1234567890123456789012345678901234567890', - ); - + const utilsModule = await import('../../../utils'); const mockError = { code: 5000, message: 'Other error' }; - vi.spyOn(getCoinbaseWalletIdentifierModule, 'generateSignatureWithCoinbaseWallet').mockRejectedValue(mockError); + vi.spyOn(utilsModule, 'web3').mockReturnValue({ + getCoinbaseWalletIdentifier: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'), + generateSignatureWithCoinbaseWallet: vi.fn().mockRejectedValue(mockError), + } as any); const signUp = new SignUp(); const result = await signUp.__internal_future.web3({ strategy: 'web3_coinbase_wallet_signature' }); diff --git a/packages/clerk-js/src/core/resources/index.ts b/packages/clerk-js/src/core/resources/index.ts index f49a5f0882e..b07196edac8 100644 --- a/packages/clerk-js/src/core/resources/index.ts +++ b/packages/clerk-js/src/core/resources/index.ts @@ -8,7 +8,6 @@ export * from './ExternalAccount'; export * from './IdentificationLink'; export * from './Image'; export * from './PhoneNumber'; -export * from './SamlAccount'; export * from './Session'; export * from './SessionWithActivities'; export * from './SignIn'; @@ -18,4 +17,3 @@ export * from './User'; export * from './Verification'; export * from './Waitlist'; export * from './Web3Wallet'; - diff --git a/packages/clerk-js/src/core/resources/internal.ts b/packages/clerk-js/src/core/resources/internal.ts index d62bd0cf1d7..3c09e804e58 100644 --- a/packages/clerk-js/src/core/resources/internal.ts +++ b/packages/clerk-js/src/core/resources/internal.ts @@ -1,47 +1,46 @@ export type { Clerk } from '../clerk'; -// Ordering matters. If you change the order of `Base` you will be fired !! jk +// Ordering matters. If you move this you will be fired !! jk export * from './Base'; -// ------- -export * from './UserSettings'; -export * from './CommerceSettings'; +// -- +export * from './APIKey'; export * from './AuthConfig'; -export * from './Client'; export * from './BillingCheckout'; -export * from './Feature'; -export * from './BillingStatement'; export * from './BillingPayment'; export * from './BillingPaymentMethod'; export * from './BillingPlan'; +export * from './BillingStatement'; export * from './BillingSubscription'; +export * from './Client'; +export * from './CommerceSettings'; export * from './DeletedObject'; export * from './DisplayConfig'; export * from './EmailAddress'; +export * from './EnterpriseAccount'; export * from './Environment'; export * from './ExternalAccount'; -export * from './EnterpriseAccount'; +export * from './Feature'; export * from './IdentificationLink'; export * from './Image'; -export * from './PhoneNumber'; export * from './Organization'; export * from './OrganizationDomain'; export * from './OrganizationInvitation'; export * from './OrganizationMembership'; export * from './OrganizationMembershipRequest'; export * from './OrganizationSuggestion'; -export * from './SamlAccount'; -export * from './Session'; export * from './Passkey'; +export * from './PhoneNumber'; export * from './ProtectConfig'; export * from './PublicUserData'; +export * from './Session'; export * from './SessionWithActivities'; export * from './SignIn'; -export * from './UserData'; export * from './SignUp'; export * from './Token'; export * from './TOTP'; export * from './User'; +export * from './UserData'; export * from './UserOrganizationInvitation'; +export * from './UserSettings'; export * from './Verification'; export * from './Web3Wallet'; -export * from './Waitlist'; -export * from './APIKey'; +export * from './Waitlist'; \ No newline at end of file diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index 7d0310161db..b200154be15 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -1,4 +1,5 @@ -import { type ClerkError, createClerkGlobalHookError, isClerkAPIResponseError } from '@clerk/shared/error'; +import type { ClerkAPIError, ClerkError } from '@clerk/shared/error'; +import { createClerkGlobalHookError, isClerkAPIResponseError } from '@clerk/shared/error'; import type { Errors, SignInErrors, SignInSignal, SignUpErrors, SignUpSignal } from '@clerk/shared/types'; import { snakeToCamel } from '@clerk/shared/underscore'; import { computed, signal } from 'alien-signals'; @@ -58,7 +59,10 @@ export function errorsToParsedErrors>( return parsedErrors; } - const hasFieldErrors = error.errors.some(error => 'meta' in error && error.meta && 'paramName' in error.meta); + function isFieldError(error: ClerkAPIError): boolean { + return 'meta' in error && error.meta && 'paramName' in error.meta && error.meta.paramName !== undefined; + } + const hasFieldErrors = error.errors.some(isFieldError); if (hasFieldErrors) { error.errors.forEach(error => { if (parsedErrors.raw) { @@ -66,7 +70,7 @@ export function errorsToParsedErrors>( } else { parsedErrors.raw = [error]; } - if ('meta' in error && error.meta && 'paramName' in error.meta) { + if (isFieldError(error)) { const name = snakeToCamel(error.meta.paramName); if (name in parsedErrors.fields) { (parsedErrors.fields as any)[name] = error; diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index 2fea2b45820..411325c068b 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -49,10 +49,18 @@ export class State implements StateInterface { private onResourceUpdated = (payload: { resource: BaseResource }) => { if (payload.resource instanceof SignIn) { + const previousResource = this.signInResourceSignal().resource; + if (shouldIgnoreNullUpdate(previousResource, payload.resource)) { + return; + } this.signInResourceSignal({ resource: payload.resource }); } if (payload.resource instanceof SignUp) { + const previousResource = this.signUpResourceSignal().resource; + if (shouldIgnoreNullUpdate(previousResource, payload.resource)) { + return; + } this.signUpResourceSignal({ resource: payload.resource }); } }; @@ -67,3 +75,13 @@ export class State implements StateInterface { } }; } + +/** + * Returns true if the new resource is null and the previous resource has not been finalized. This is used to prevent + * nullifying the resource after it's been completed. + */ +function shouldIgnoreNullUpdate(previousResource: SignIn | null, newResource: SignIn | null): boolean; +function shouldIgnoreNullUpdate(previousResource: SignUp | null, newResource: SignUp | null): boolean; +function shouldIgnoreNullUpdate(previousResource: SignIn | SignUp | null, newResource: SignIn | SignUp | null) { + return !newResource?.id && previousResource && previousResource.__internal_future?.hasBeenFinalized === false; +} diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts index ac0621a4160..dc18ffaced5 100644 --- a/packages/clerk-js/src/core/warnings.ts +++ b/packages/clerk-js/src/core/warnings.ts @@ -1,65 +1,2 @@ -import type { Serializable } from '@clerk/shared/types'; - -const formatWarning = (msg: string) => { - return `🔒 Clerk:\n${msg.trim()}\n(This notice only appears in development)`; -}; - -const createMessageForDisabledOrganizations = ( - componentName: - | 'OrganizationProfile' - | 'OrganizationSwitcher' - | 'OrganizationList' - | 'CreateOrganization' - | 'TaskChooseOrganization', -) => { - return formatWarning( - `The <${componentName}/> cannot be rendered when the feature is turned off. Visit 'dashboard.clerk.com' to enable the feature. Since the feature is turned off, this is no-op.`, - ); -}; -const createMessageForDisabledBilling = (componentName: 'PricingTable' | 'Checkout' | 'PlanDetails') => { - return formatWarning( - `The <${componentName}/> component cannot be rendered when billing is disabled. Visit 'https://dashboard.clerk.com/last-active?path=billing/settings' to follow the necessary steps to enable billing. Since billing is disabled, this is no-op.`, - ); -}; -const warnings = { - cannotRenderComponentWhenSessionExists: - 'The and components cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the Home URL instead.', - cannotRenderSignUpComponentWhenSessionExists: - 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the value set in `afterSignUp` URL instead.', - cannotRenderSignUpComponentWhenTaskExists: - 'The component cannot render when a user has a pending task, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the task instead.', - cannotRenderComponentWhenTaskDoesNotExist: - ' cannot render unless a session task is pending. Clerk is redirecting to the value set in `redirectUrlComplete` instead.', - cannotRenderSignInComponentWhenSessionExists: - 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `afterSignIn` URL instead.', - cannotRenderSignInComponentWhenTaskExists: - 'The component cannot render when a user has a pending task, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the task instead.', - cannotRenderComponentWhenUserDoesNotExist: - ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', - cannotRenderComponentWhenOrgDoesNotExist: ` cannot render unless an organization is active. Since no organization is currently active, this is no-op.`, - cannotRenderAnyOrganizationComponent: createMessageForDisabledOrganizations, - cannotRenderAnyBillingComponent: createMessageForDisabledBilling, - cannotOpenUserProfile: - 'The UserProfile modal cannot render unless a user is signed in. Since no user is signed in, this is no-op.', - cannotOpenCheckout: - 'The Checkout drawer cannot render unless a user is signed in. Since no user is signed in, this is no-op.', - cannotOpenSignInOrSignUp: - 'The SignIn or SignUp modals do not render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, this is no-op.', - cannotRenderAPIKeysComponent: - 'The component cannot be rendered when API keys are disabled. Since API keys are disabled, this is no-op.', - cannotRenderAPIKeysComponentForUserWhenDisabled: - 'The component cannot be rendered when user API keys are disabled. Since user API keys are disabled, this is no-op.', - cannotRenderAPIKeysComponentForOrgWhenDisabled: - 'The component cannot be rendered when organization API keys are disabled. Since organization API keys are disabled, this is no-op.', -}; - -type SerializableWarnings = Serializable; - -for (const key of Object.keys(warnings)) { - const item = warnings[key as keyof typeof warnings]; - if (typeof item !== 'function') { - warnings[key as keyof SerializableWarnings] = formatWarning(item); - } -} - -export { warnings }; +// Re-export warnings from shared package +export { warnings } from '@clerk/shared/internal/clerk-js/warnings'; diff --git a/packages/clerk-js/src/global.d.ts b/packages/clerk-js/src/global.d.ts index 7751ef82378..11daf167ad0 100644 --- a/packages/clerk-js/src/global.d.ts +++ b/packages/clerk-js/src/global.d.ts @@ -3,17 +3,18 @@ declare module '*.svg' { export default value; } -declare const __PKG_NAME__: string; -declare const __PKG_VERSION__: string; -declare const __DEV__: boolean; +const __PKG_NAME__: string; +const __PKG_VERSION__: string; +const __DEV__: boolean; /** * Build time feature flags. */ -declare const __BUILD_DISABLE_RHC__: string; -declare const __BUILD_VARIANT_CHIPS__: boolean; +const __BUILD_DISABLE_RHC__: string; +const __BUILD_VARIANT_CHANNEL__: boolean; +const __BUILD_VARIANT_CHIPS__: boolean; interface Window { - __unstable__onBeforeSetActive: (intent?: 'sign-out') => Promise | void; - __unstable__onAfterSetActive: () => Promise | void; + __internal_onBeforeSetActive: (intent?: 'sign-out') => Promise | void; + __internal_onAfterSetActive: () => Promise | void; } diff --git a/packages/clerk-js/src/index.browser.ts b/packages/clerk-js/src/index.browser.ts index 82bd326c1a6..66647f279f6 100644 --- a/packages/clerk-js/src/index.browser.ts +++ b/packages/clerk-js/src/index.browser.ts @@ -1,14 +1,10 @@ // It's crucial this is the first import, // otherwise chunk loading will not work -// eslint-disable-next-line + import './utils/setWebpackChunkPublicPath'; import { Clerk } from './core/clerk'; -import { mountComponentRenderer } from './ui/Components'; - -Clerk.mountComponentRenderer = mountComponentRenderer; - const publishableKey = document.querySelector('script[data-clerk-publishable-key]')?.getAttribute('data-clerk-publishable-key') || window.__clerk_publishable_key || @@ -26,7 +22,6 @@ const domain = if (!window.Clerk) { window.Clerk = new Clerk(publishableKey, { proxyUrl, - // @ts-expect-error domain, }); } diff --git a/packages/clerk-js/src/index.headless.browser.ts b/packages/clerk-js/src/index.headless.browser.ts index 4151921ff3c..fbdd2407a53 100644 --- a/packages/clerk-js/src/index.headless.browser.ts +++ b/packages/clerk-js/src/index.headless.browser.ts @@ -23,7 +23,6 @@ const domain = if (!window.Clerk) { window.Clerk = new Clerk(publishableKey, { proxyUrl, - // @ts-expect-error domain, }); } diff --git a/packages/clerk-js/src/index.legacy.browser.ts b/packages/clerk-js/src/index.legacy.browser.ts index 7d0d031c6cc..73dab30df70 100644 --- a/packages/clerk-js/src/index.legacy.browser.ts +++ b/packages/clerk-js/src/index.legacy.browser.ts @@ -7,10 +7,6 @@ import 'regenerator-runtime/runtime'; import { Clerk } from './core/clerk'; -import { mountComponentRenderer } from './ui/Components'; - -Clerk.mountComponentRenderer = mountComponentRenderer; - const publishableKey = document.querySelector('script[data-clerk-publishable-key]')?.getAttribute('data-clerk-publishable-key') || window.__clerk_publishable_key || @@ -28,7 +24,6 @@ const domain = if (!window.Clerk) { window.Clerk = new Clerk(publishableKey, { proxyUrl, - // @ts-expect-error domain, }); } diff --git a/packages/clerk-js/src/index.ts b/packages/clerk-js/src/index.ts index 2f9acf96b6e..f7ad89e436d 100644 --- a/packages/clerk-js/src/index.ts +++ b/packages/clerk-js/src/index.ts @@ -1,7 +1,6 @@ import 'regenerator-runtime/runtime'; import { Clerk } from './core/clerk'; -import { mountComponentRenderer } from './ui/Components'; export { ClerkAPIResponseError, @@ -19,8 +18,6 @@ export { } from '@clerk/shared/error'; export { Clerk }; -Clerk.mountComponentRenderer = mountComponentRenderer; - if (module.hot) { module.hot.accept(); } diff --git a/packages/clerk-js/src/test/create-fixtures.tsx b/packages/clerk-js/src/test/create-fixtures.tsx index e9af007e3aa..2ffa7c64034 100644 --- a/packages/clerk-js/src/test/create-fixtures.tsx +++ b/packages/clerk-js/src/test/create-fixtures.tsx @@ -1,3 +1,6 @@ +/* eslint-disable */ +// @ts-nocheck + import type { ClerkOptions, ClientJSON, EnvironmentJSON, LoadedClerk } from '@clerk/shared/types'; import { useState } from 'react'; import { vi } from 'vitest'; diff --git a/packages/clerk-js/src/test/fixture-helpers.ts b/packages/clerk-js/src/test/fixture-helpers.ts index 1a3655d754c..86547dae2c0 100644 --- a/packages/clerk-js/src/test/fixture-helpers.ts +++ b/packages/clerk-js/src/test/fixture-helpers.ts @@ -8,7 +8,6 @@ import type { OrganizationEnrollmentMode, PhoneNumberJSON, PublicUserDataJSON, - SamlAccountJSON, SessionJSON, SignInJSON, SignUpJSON, @@ -44,13 +43,12 @@ export const createClientFixtureHelpers = (baseClient: ClientJSON) => { const createUserFixtureHelpers = (baseClient: ClientJSON) => { type WithUserParams = Omit< Partial, - 'email_addresses' | 'phone_numbers' | 'external_accounts' | 'saml_accounts' | 'organization_memberships' + 'email_addresses' | 'phone_numbers' | 'external_accounts' | 'organization_memberships' > & { identifier?: string; email_addresses?: Array>; phone_numbers?: Array>; external_accounts?: Array>; - saml_accounts?: Array>; organization_memberships?: Array; tasks?: SessionJSON['tasks']; }; diff --git a/packages/clerk-js/src/test/fixtures.ts b/packages/clerk-js/src/test/fixtures.ts index 9b3403f0b57..f8596b5329c 100644 --- a/packages/clerk-js/src/test/fixtures.ts +++ b/packages/clerk-js/src/test/fixtures.ts @@ -1,3 +1,6 @@ +/* eslint-disable */ +// @ts-nocheck + import type { AuthConfigJSON, ClientJSON, diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/BillingWidget.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/BillingWidget.tsx deleted file mode 100644 index 5f6c3cf4430..00000000000 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/BillingWidget.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { runIfFunctionOrReturn } from '../../../utils'; -import { AlertIcon, Flex, Link, Text } from '../../customizables'; -import { useRouter } from '../../router'; - -export const BillingWidget = ({ - __unstable_manageBillingUrl, - __unstable_manageBillingMembersLimit, -}: { - __unstable_manageBillingUrl: string | ((args: any) => string); - __unstable_manageBillingMembersLimit: string | ((args: any) => string); -}) => { - const router = useRouter(); - - return ( - ({ - background: theme.colors.$neutralAlpha50, - padding: theme.space.$4, - borderRadius: theme.radii.$md, - })} - > - ({ marginTop: t.space.$1 })} - /> - - This organization is limited to {runIfFunctionOrReturn(__unstable_manageBillingMembersLimit)} members. -
- ({ - alignSelf: 'flex-start', - color: t.colors.$primary500, - fontWeight: t.fontWeights.$normal, - })} - onClick={() => router.navigate(runIfFunctionOrReturn(__unstable_manageBillingUrl))} - > - Upgrade for unlimited members - -
-
- ); -}; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/MembershipWidget.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/MembershipWidget.tsx deleted file mode 100644 index 82978d49d74..00000000000 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/MembershipWidget.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useOrganization } from '@clerk/shared/react'; - -import { Gauge } from '@/ui/elements/Gauge'; - -import { runIfFunctionOrReturn } from '../../../utils'; -import { useOrganizationProfileContext } from '../../contexts'; -import { Flex, Icon, Link, Text } from '../../customizables'; -import { ArrowRightIcon } from '../../icons'; -import { useRouter } from '../../router'; -import { mqu } from '../../styledSystem'; - -export const MembershipWidget = () => { - const { organization } = useOrganization(); - //@ts-expect-error - const { __unstable_manageBillingUrl, __unstable_manageBillingLabel, __unstable_manageBillingMembersLimit } = - useOrganizationProfileContext(); - const router = useRouter(); - - if (!organization) { - return null; - } - - const totalCount = organization?.membersCount + organization?.pendingInvitationsCount; - const limit = runIfFunctionOrReturn(__unstable_manageBillingMembersLimit); - - return ( - ({ - background: theme.colors.$neutralAlpha50, - padding: theme.space.$2, - borderRadius: theme.radii.$md, - gap: theme.space.$4, - })} - > - ({ - [mqu.sm]: { - gap: t.space.$3, - }, - gap: t.space.$2, - })} - > - {limit > 0 && ( - - )} - ({ - [mqu.sm]: { - flexDirection: 'column', - }, - gap: t.space.$0x5, - })} - > - You can invite {__unstable_manageBillingMembersLimit ? `up to ${limit}` : 'unlimited'} members. - {limit > 0 && ( - ({ - fontWeight: t.fontWeights.$medium, - })} - variant='body' - onClick={() => router.navigate(runIfFunctionOrReturn(__unstable_manageBillingUrl))} - > - {runIfFunctionOrReturn(__unstable_manageBillingLabel) || 'Manage billing'} - - - - )} - - - - ); -}; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/index.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/index.ts deleted file mode 100644 index c78a073ba17..00000000000 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { withTaskGuard } from './withTaskGuard'; diff --git a/packages/clerk-js/src/ui/components/SignUp/util.ts b/packages/clerk-js/src/ui/components/SignUp/util.ts deleted file mode 100644 index 343a9bcaccc..00000000000 --- a/packages/clerk-js/src/ui/components/SignUp/util.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../../../utils/completeSignUpFlow'; diff --git a/packages/clerk-js/src/ui/localization/defaultEnglishResource.ts b/packages/clerk-js/src/ui/localization/defaultEnglishResource.ts deleted file mode 100644 index 81219b7f1ef..00000000000 --- a/packages/clerk-js/src/ui/localization/defaultEnglishResource.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { enUS } from '@clerk/localizations'; -import type { DeepRequired } from '@clerk/shared/types'; - -export const defaultResource = enUS as DeepRequired; diff --git a/packages/clerk-js/src/utils/__tests__/appearance.test.ts b/packages/clerk-js/src/utils/__tests__/appearance.test.ts deleted file mode 100644 index a79e1f9ab36..00000000000 --- a/packages/clerk-js/src/utils/__tests__/appearance.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -import type { Appearance, BaseTheme } from '@clerk/shared/types'; -import { describe, expect, it } from 'vitest'; - -import { processCssLayerNameExtraction } from '../appearance'; - -describe('processCssLayerNameExtraction', () => { - it('extracts cssLayerName from single baseTheme and moves it to appearance level', () => { - const appearance: Appearance = { - baseTheme: { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'theme-layer', - }, - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('theme-layer'); - expect(result?.baseTheme).toBeDefined(); - if (result?.baseTheme && !Array.isArray(result.baseTheme)) { - expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - expect(result.baseTheme.__type).toBe('prebuilt_appearance'); - } - }); - - it('preserves appearance-level cssLayerName over baseTheme cssLayerName', () => { - const appearance: Appearance = { - cssLayerName: 'appearance-layer', - baseTheme: { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'theme-layer', - }, - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('appearance-layer'); - if (result?.baseTheme && !Array.isArray(result.baseTheme)) { - expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - } - }); - - it('extracts cssLayerName from first theme in array that has one', () => { - const appearance: Appearance = { - baseTheme: [ - { - __type: 'prebuilt_appearance' as const, - }, - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'first-layer', - }, - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'second-layer', - }, - ], - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('first-layer'); - expect(result?.baseTheme).toBeDefined(); - if (result?.baseTheme && Array.isArray(result.baseTheme)) { - expect(result.baseTheme).toHaveLength(3); - expect((result.baseTheme[0] as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - expect((result.baseTheme[1] as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - expect((result.baseTheme[2] as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - result.baseTheme.forEach(theme => { - expect(theme.__type).toBe('prebuilt_appearance'); - }); - } - }); - - it('preserves appearance-level cssLayerName over array baseTheme cssLayerName', () => { - const appearance: Appearance = { - cssLayerName: 'appearance-layer', - baseTheme: [ - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'theme1-layer', - }, - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'theme2-layer', - }, - ], - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('appearance-layer'); - if (result?.baseTheme && Array.isArray(result.baseTheme)) { - result.baseTheme.forEach(theme => { - expect((theme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - }); - } - }); - - it('handles single baseTheme without cssLayerName', () => { - const appearance: Appearance = { - baseTheme: { - __type: 'prebuilt_appearance' as const, - }, - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBeUndefined(); - if (result?.baseTheme && !Array.isArray(result.baseTheme)) { - expect(result.baseTheme.__type).toBe('prebuilt_appearance'); - expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - } - }); - - it('handles array of baseThemes without any cssLayerName', () => { - const appearance: Appearance = { - baseTheme: [ - { - __type: 'prebuilt_appearance' as const, - }, - { - __type: 'prebuilt_appearance' as const, - }, - ], - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBeUndefined(); - if (result?.baseTheme && Array.isArray(result.baseTheme)) { - expect(result.baseTheme).toHaveLength(2); - result.baseTheme.forEach(theme => { - expect(theme.__type).toBe('prebuilt_appearance'); - expect((theme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - }); - } - }); - - it('handles no baseTheme provided', () => { - const appearance: Appearance = { - cssLayerName: 'standalone-layer', - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('standalone-layer'); - expect(result?.baseTheme).toBeUndefined(); - }); - - it('handles undefined appearance', () => { - const result = processCssLayerNameExtraction(undefined); - - expect(result).toBeUndefined(); - }); - - it('preserves other appearance properties', () => { - const appearance: Appearance = { - variables: { colorPrimary: 'blue' }, - baseTheme: { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'theme-layer', - }, - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('theme-layer'); - expect(result?.variables?.colorPrimary).toBe('blue'); - if (result?.baseTheme && !Array.isArray(result.baseTheme)) { - expect((result.baseTheme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - } - }); - - it('handles empty baseTheme array', () => { - const appearance: Appearance = { - baseTheme: [], - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBeUndefined(); - expect(result?.baseTheme).toEqual([]); - expect(Array.isArray(result?.baseTheme)).toBe(true); - }); - - it('uses first valid cssLayerName from mixed array when appearance.cssLayerName is absent', () => { - const appearance: Appearance = { - baseTheme: [ - { - __type: 'prebuilt_appearance' as const, - // No cssLayerName in first theme - }, - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'second-theme-layer', - }, - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'third-theme-layer', - }, - ], - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('second-theme-layer'); - expect(Array.isArray(result?.baseTheme)).toBe(true); - if (Array.isArray(result?.baseTheme)) { - expect(result.baseTheme).toHaveLength(3); - // Check that cssLayerName was removed from all themes - result.baseTheme.forEach(theme => { - expect((theme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - }); - } - }); - - it('preserves appearance.cssLayerName over baseTheme array cssLayerName', () => { - const appearance: Appearance = { - cssLayerName: 'appearance-level-layer', - baseTheme: [ - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'theme-layer-1', - }, - { - __type: 'prebuilt_appearance' as const, - cssLayerName: 'theme-layer-2', - }, - ], - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBe('appearance-level-layer'); - expect(Array.isArray(result?.baseTheme)).toBe(true); - if (Array.isArray(result?.baseTheme)) { - expect(result.baseTheme).toHaveLength(2); - // Check that cssLayerName was removed from all themes - result.baseTheme.forEach(theme => { - expect((theme as BaseTheme & { cssLayerName?: string }).cssLayerName).toBeUndefined(); - }); - } - }); - - it('returns single theme unchanged when it has no cssLayerName', () => { - const appearance: Appearance = { - baseTheme: { - __type: 'prebuilt_appearance' as const, - // No cssLayerName property - }, - }; - - const result = processCssLayerNameExtraction(appearance); - - expect(result?.cssLayerName).toBeUndefined(); - expect(result?.baseTheme).toEqual({ - __type: 'prebuilt_appearance', - }); - expect(Array.isArray(result?.baseTheme)).toBe(false); - }); -}); diff --git a/packages/clerk-js/src/utils/__tests__/captcha.test.ts b/packages/clerk-js/src/utils/__tests__/captcha.test.ts index 97b916e7802..f2e65933bd9 100644 --- a/packages/clerk-js/src/utils/__tests__/captcha.test.ts +++ b/packages/clerk-js/src/utils/__tests__/captcha.test.ts @@ -35,7 +35,7 @@ describe('Nonce support', () => { it('should extract nonce from clerk options when available', async () => { // Mock clerk instance with internal options const mockClerk = { - __unstable__environment: { + __internal_environment: { displayConfig: { captchaProvider: 'turnstile', captchaPublicKey: 'test-site-key', @@ -63,7 +63,7 @@ describe('Nonce support', () => { it('should return undefined nonce when not available in clerk options', async () => { const mockClerk = { - __unstable__environment: { + __internal_environment: { displayConfig: { captchaProvider: 'turnstile', captchaPublicKey: 'test-site-key', @@ -88,7 +88,7 @@ describe('Nonce support', () => { it('should handle clerk instance without __internal_getOption method', async () => { const mockClerk = { - __unstable__environment: { + __internal_environment: { displayConfig: { captchaProvider: 'turnstile', captchaPublicKey: 'test-site-key', @@ -152,7 +152,7 @@ describe('Nonce support', () => { beforeEach(() => { // Mock clerk instance mockClerk = { - __unstable__environment: { + __internal_environment: { displayConfig: { captchaProvider: 'turnstile', captchaPublicKey: 'test-site-key', diff --git a/packages/clerk-js/src/utils/assertNoLegacyProp.ts b/packages/clerk-js/src/utils/assertNoLegacyProp.ts deleted file mode 100644 index a574b8d9e2f..00000000000 --- a/packages/clerk-js/src/utils/assertNoLegacyProp.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { logger } from '@clerk/shared/logger'; - -export function assertNoLegacyProp(props: Record) { - const legacyProps = ['redirectUrl', 'afterSignInUrl', 'afterSignUpUrl', 'after_sign_in_url', 'after_sign_up_url']; - const legacyProp = Object.keys(props).find(key => legacyProps.includes(key)); - - if (legacyProp && props[legacyProp]) { - logger.warnOnce( - `Clerk: The prop "${legacyProp}" is deprecated and should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead. Learn more: https://clerk.com/docs/guides/custom-redirects#redirect-url-props`, - ); - } -} - -export function warnForNewPropShadowingLegacyProp( - newKey: string | undefined, - newValue: string | undefined | null, - legacyKey: string | undefined, - legacyValue: string | undefined | null, -) { - if (newValue && legacyValue) { - logger.warnOnce( - `Clerk: The "${newKey}" prop ("${newValue}") has priority over the legacy "${legacyKey}" (or "redirectUrl") ("${legacyValue}"), which will be completely ignored in this case. "${legacyKey}" (or "redirectUrl" prop) should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead. Learn more: https://clerk.com/docs/guides/custom-redirects#redirect-url-props`, - ); - } -} diff --git a/packages/clerk-js/src/utils/beforeUnloadTracker.ts b/packages/clerk-js/src/utils/beforeUnloadTracker.ts index 59094c63bed..a9ce63b70f1 100644 --- a/packages/clerk-js/src/utils/beforeUnloadTracker.ts +++ b/packages/clerk-js/src/utils/beforeUnloadTracker.ts @@ -1,4 +1,4 @@ -import { CLERK_BEFORE_UNLOAD_EVENT } from './windowNavigate'; +import { CLERK_BEFORE_UNLOAD_EVENT } from '@clerk/shared/internal/clerk-js/windowNavigate'; /** * Tracks beforeUnload events. diff --git a/packages/clerk-js/src/utils/captcha/constants.ts b/packages/clerk-js/src/utils/captcha/constants.ts deleted file mode 100644 index 7ad0a0e05c0..00000000000 --- a/packages/clerk-js/src/utils/captcha/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CAPTCHA_ELEMENT_ID = 'clerk-captcha'; -export const CAPTCHA_INVISIBLE_CLASSNAME = 'clerk-invisible-captcha'; diff --git a/packages/clerk-js/src/utils/captcha/retrieveCaptchaInfo.ts b/packages/clerk-js/src/utils/captcha/retrieveCaptchaInfo.ts index 2985d4ce9c0..b949925ef0c 100644 --- a/packages/clerk-js/src/utils/captcha/retrieveCaptchaInfo.ts +++ b/packages/clerk-js/src/utils/captcha/retrieveCaptchaInfo.ts @@ -1,7 +1,7 @@ import type { Clerk } from '../../core/clerk'; export const retrieveCaptchaInfo = (clerk: Clerk) => { - const _environment = clerk.__unstable__environment; + const _environment = clerk.__internal_environment; const captchaProvider = _environment ? _environment.displayConfig.captchaProvider : 'turnstile'; // Access nonce via internal options - casting to any since nonce is in IsomorphicClerkOptions but not ClerkOptions diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 1a21f57d1dc..bd0a5ad03a5 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -1,18 +1,19 @@ import { waitForElement } from '@clerk/shared/dom'; +import { CAPTCHA_ELEMENT_ID, CAPTCHA_INVISIBLE_CLASSNAME } from '@clerk/shared/internal/clerk-js/constants'; import { loadScript } from '@clerk/shared/loadScript'; -import type { CaptchaAppearanceOptions, CaptchaWidgetType } from '@clerk/shared/types'; +import type { CaptchaWidgetType } from '@clerk/shared/types'; -import { CAPTCHA_ELEMENT_ID, CAPTCHA_INVISIBLE_CLASSNAME } from './constants'; import type { CaptchaOptions } from './types'; // We use the explicit render mode to be able to control when the widget is rendered. // CF docs: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#disable-implicit-rendering const CLOUDFLARE_TURNSTILE_ORIGINAL_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; +// These belong to `clerk/ui` now type CaptchaAttributes = { - theme?: CaptchaAppearanceOptions['theme']; - language?: CaptchaAppearanceOptions['language']; - size: CaptchaAppearanceOptions['size']; + theme?: unknown; + language?: unknown; + size: unknown; }; declare global { @@ -53,9 +54,9 @@ async function loadCaptchaFromCloudflareURL(nonce?: string) { function getCaptchaAttibutesFromElemenet(element: HTMLElement): CaptchaAttributes { try { - const theme = (element.getAttribute('data-cl-theme') as CaptchaAppearanceOptions['theme']) || undefined; - const language = (element.getAttribute('data-cl-language') as CaptchaAppearanceOptions['language']) || undefined; - const size = (element.getAttribute('data-cl-size') as CaptchaAppearanceOptions['size']) || undefined; + const theme = element.getAttribute('data-cl-theme') || undefined; + const language = element.getAttribute('data-cl-language') || undefined; + const size = element.getAttribute('data-cl-size') || undefined; return { theme, language, size }; } catch { @@ -79,9 +80,9 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { let captchaToken = ''; let id = ''; let turnstileSiteKey = siteKey; - let captchaTheme: CaptchaAppearanceOptions['theme']; - let captchaSize: CaptchaAppearanceOptions['size']; - let captchaLanguage: CaptchaAppearanceOptions['language']; + let captchaTheme: any; + let captchaSize: any; + let captchaLanguage: any; let retries = 0; let widgetContainerQuerySelector: string | undefined; // The backend uses this to determine which Turnstile site-key was used in order to verify the token diff --git a/packages/clerk-js/src/utils/index.ts b/packages/clerk-js/src/utils/index.ts index 37bd8d49ff6..2a66443941e 100644 --- a/packages/clerk-js/src/utils/index.ts +++ b/packages/clerk-js/src/utils/index.ts @@ -1,30 +1,23 @@ -export * from './appearance'; export * from './beforeUnloadTracker'; export * from './billing'; -export * from './completeSignUpFlow'; -export * from './componentGuards'; -export * from './dynamicParamParser'; -export * from './email'; -export * from './encoders'; +export * from '@clerk/shared/internal/clerk-js/completeSignUpFlow'; +export * from '@clerk/shared/internal/clerk-js/email'; +export * from '@clerk/shared/internal/clerk-js/encoders'; export * from './errors'; export * from './errorThrower'; export * from './filterUndefinedValues'; -export * from './getClerkQueryParam'; -export * from './hex'; +export * from '@clerk/shared/internal/clerk-js/queryParams'; export * from './ignoreEventValue'; -export * from './image'; export * from './instance'; export * from './jwt'; export * from './locale'; -export * from './normalizeRoutingOptions'; -export * from './organization'; +export * from '@clerk/shared/internal/clerk-js/organization'; export * from './pageLifecycle'; -export * from './path'; -export * from './props'; -export * from './queryStateParams'; -export * from './querystring'; -export * from './runtime'; +export * from '@clerk/shared/internal/clerk-js/path'; +export * from '@clerk/shared/internal/clerk-js/queryStateParams'; +export * from '@clerk/shared/internal/clerk-js/querystring'; +export * from '@clerk/shared/internal/clerk-js/runtime'; export * from './tokenId'; -export * from './url'; +export * from '@clerk/shared/internal/clerk-js/url'; export * from './web3'; -export * from './windowNavigate'; +export * from '@clerk/shared/internal/clerk-js/windowNavigate'; diff --git a/packages/clerk-js/src/utils/instance.ts b/packages/clerk-js/src/utils/instance.ts index 404a81608ec..97b869e7675 100644 --- a/packages/clerk-js/src/utils/instance.ts +++ b/packages/clerk-js/src/utils/instance.ts @@ -1,4 +1,4 @@ -import { isDevOrStagingUrl } from './url'; +import { isDevOrStagingUrl } from '@clerk/shared/internal/clerk-js/url'; const FRONTEND_API_DEV_OR_STG_REGEX = /^clerk\.([\w|-]+\.){2,4}(dev|com)$/i; diff --git a/packages/clerk-js/src/utils/jwt.ts b/packages/clerk-js/src/utils/jwt.ts index 9bac22a6274..e921ddb8912 100644 --- a/packages/clerk-js/src/utils/jwt.ts +++ b/packages/clerk-js/src/utils/jwt.ts @@ -1,7 +1,6 @@ +import { urlDecodeB64 } from '@clerk/shared/internal/clerk-js/encoders'; import type { JWT, JwtPayload } from '@clerk/shared/types'; -import { urlDecodeB64 } from './encoders'; - export function decode(token: string): JWT { const parts = (token || '').split('.'); const [header, payload, signature] = parts; diff --git a/packages/clerk-js/src/utils/moduleManager.ts b/packages/clerk-js/src/utils/moduleManager.ts new file mode 100644 index 00000000000..ee9859aa2ac --- /dev/null +++ b/packages/clerk-js/src/utils/moduleManager.ts @@ -0,0 +1,18 @@ +import type { ImportableModule, ModuleManager as ModuleManagerI } from '@clerk/shared/moduleManager'; +import { safeImport } from '@clerk/shared/safeImport'; + +export class ModuleManager implements ModuleManagerI { + #importMap = { + '@zxcvbn-ts/core': () => safeImport(() => import('@zxcvbn-ts/core')), + '@zxcvbn-ts/language-common': () => safeImport(() => import('@zxcvbn-ts/language-common')), + '@base-org/account': () => safeImport(() => import('@base-org/account')), + '@coinbase/wallet-sdk': () => safeImport(() => import('@coinbase/wallet-sdk')), + '@stripe/stripe-js': () => safeImport(() => import('@stripe/stripe-js')), + } satisfies Record Promise>; + + import(module: ImportableModule) { + // Not typing this as any because we want to allow any type to be returned from the interface this class implements insetad of + // defining the types twice + return (this.#importMap[module] ? this.#importMap[module]() : Promise.resolve(undefined)) as any; + } +} diff --git a/packages/clerk-js/src/utils/web3.ts b/packages/clerk-js/src/utils/web3.ts index fc494876f78..7e0a2fbd766 100644 --- a/packages/clerk-js/src/utils/web3.ts +++ b/packages/clerk-js/src/utils/web3.ts @@ -1,124 +1,5 @@ -import type { Web3Provider } from '@clerk/shared/types'; +import { createWeb3 } from '@clerk/shared/internal/clerk-js/web3'; -import { clerkUnsupportedEnvironmentWarning } from '@/core/errors'; +import { ModuleManager } from './moduleManager'; -import { toHex } from './hex'; -import { getInjectedWeb3Providers } from './injectedWeb3Providers'; - -type GetWeb3IdentifierParams = { - provider: Web3Provider; -}; - -export async function getWeb3Identifier(params: GetWeb3IdentifierParams): Promise { - const { provider } = params; - const ethereum = await getEthereumProvider(provider); - - // TODO - core-3: Improve error handling for the case when the provider is not found - if (!ethereum) { - // If a plugin for the requested provider is not found, - // the flow will fail as it has been the expected behavior so far. - return ''; - } - - const identifiers = await ethereum.request({ method: 'eth_requestAccounts' }); - // @ts-ignore -- Provider SDKs may return unknown shape; use first address if present - return (identifiers && identifiers[0]) || ''; -} - -type GenerateWeb3SignatureParams = GenerateSignatureParams & { - provider: Web3Provider; -}; - -export async function generateWeb3Signature(params: GenerateWeb3SignatureParams): Promise { - const { identifier, nonce, provider } = params; - const ethereum = await getEthereumProvider(provider); - - // TODO - core-3: Improve error handling for the case when the provider is not found - if (!ethereum) { - // If a plugin for the requested provider is not found, - // the flow will fail as it has been the expected behavior so far. - return ''; - } - - return await ethereum.request({ - method: 'personal_sign', - params: [`0x${toHex(nonce)}`, identifier], - }); -} - -export async function getMetamaskIdentifier(): Promise { - return await getWeb3Identifier({ provider: 'metamask' }); -} - -export async function getCoinbaseWalletIdentifier(): Promise { - return await getWeb3Identifier({ provider: 'coinbase_wallet' }); -} - -export async function getOKXWalletIdentifier(): Promise { - return await getWeb3Identifier({ provider: 'okx_wallet' }); -} - -export async function getBaseIdentifier(): Promise { - return await getWeb3Identifier({ provider: 'base' }); -} - -type GenerateSignatureParams = { - identifier: string; - nonce: string; -}; - -export async function generateSignatureWithMetamask(params: GenerateSignatureParams): Promise { - return await generateWeb3Signature({ ...params, provider: 'metamask' }); -} - -export async function generateSignatureWithCoinbaseWallet(params: GenerateSignatureParams): Promise { - return await generateWeb3Signature({ ...params, provider: 'coinbase_wallet' }); -} - -export async function generateSignatureWithOKXWallet(params: GenerateSignatureParams): Promise { - return await generateWeb3Signature({ ...params, provider: 'okx_wallet' }); -} - -export async function generateSignatureWithBase(params: GenerateSignatureParams): Promise { - return await generateWeb3Signature({ ...params, provider: 'base' }); -} - -async function getEthereumProvider(provider: Web3Provider) { - if (provider === 'coinbase_wallet') { - if (__BUILD_DISABLE_RHC__) { - clerkUnsupportedEnvironmentWarning('Coinbase Wallet'); - return null; - } - - const createCoinbaseWalletSDK = await import('@coinbase/wallet-sdk').then(mod => mod.createCoinbaseWalletSDK); - const sdk = createCoinbaseWalletSDK({ - preference: { - options: 'all', - }, - }); - return sdk.getProvider(); - } - if (provider === 'base') { - if (__BUILD_DISABLE_RHC__) { - clerkUnsupportedEnvironmentWarning('Base'); - return null; - } - - try { - const createBaseAccountSDK = await import('@base-org/account').then(mod => mod.createBaseAccountSDK); - - const sdk = createBaseAccountSDK({ - appName: - (typeof window !== 'undefined' && - (window.Clerk as any)?.__unstable__environment?.displayConfig?.applicationName) || - (typeof document !== 'undefined' && document.title) || - 'Web3 Application', - }); - return sdk.getProvider(); - } catch { - return null; - } - } - - return getInjectedWeb3Providers().get(provider); -} +export const web3 = () => createWeb3(new ModuleManager()); diff --git a/packages/clerk-js/src/utils/zxcvbn.ts b/packages/clerk-js/src/utils/zxcvbn.ts index 546fb23cf02..70eb563d916 100644 --- a/packages/clerk-js/src/utils/zxcvbn.ts +++ b/packages/clerk-js/src/utils/zxcvbn.ts @@ -1,19 +1,7 @@ -import type { ZxcvbnResult } from '@clerk/shared/types'; +import { createLoadZxcvbn } from '@clerk/shared/internal/clerk-js/passwords/loadZxcvbn'; -export type zxcvbnFN = (password: string, userInputs?: (string | number)[]) => ZxcvbnResult; +import { ModuleManager } from './moduleManager'; export const loadZxcvbn = () => { - return Promise.all([import('@zxcvbn-ts/core'), import('@zxcvbn-ts/language-common')]).then( - ([coreModule, languageCommonModule]) => { - const { zxcvbnOptions, zxcvbn } = coreModule; - const { dictionary, adjacencyGraphs } = languageCommonModule; - zxcvbnOptions.setOptions({ - dictionary: { - ...dictionary, - }, - graphs: adjacencyGraphs, - }); - return zxcvbn; - }, - ); + return createLoadZxcvbn(new ModuleManager()).loadZxcvbn; }; diff --git a/packages/clerk-js/tsconfig.json b/packages/clerk-js/tsconfig.json index 96bcc440b5b..896a6e8665a 100644 --- a/packages/clerk-js/tsconfig.json +++ b/packages/clerk-js/tsconfig.json @@ -24,5 +24,5 @@ "@/*": ["./src/*"] } }, - "include": ["src", "vitest.config.mts", "vitest.setup.mts"] + "include": ["src", "vitest.config.mts", "vitest.setup.mts", "../shared/internal/clerk-js/componentGuards.ts"] } diff --git a/packages/clerk-js/turbo.json b/packages/clerk-js/turbo.json index a6e53939738..10c4d163114 100644 --- a/packages/clerk-js/turbo.json +++ b/packages/clerk-js/turbo.json @@ -22,7 +22,7 @@ ] }, "build:sandbox": { - "dependsOn": ["^build"], + "dependsOn": ["^build", "@clerk/localizations#build"], "inputs": ["sandbox/**"], "outputs": ["dist/**"] }, diff --git a/packages/dev-cli/package.json b/packages/dev-cli/package.json index 6a77669275b..5da57139a34 100644 --- a/packages/dev-cli/package.json +++ b/packages/dev-cli/package.json @@ -32,7 +32,7 @@ }, "devDependencies": {}, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" }, "publishConfig": { "access": "public" diff --git a/packages/elements/.gitignore b/packages/elements/.gitignore deleted file mode 100644 index 912a0263905..00000000000 --- a/packages/elements/.gitignore +++ /dev/null @@ -1,65 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env.local - -# Distribution directories -dist/ -.next - -# Mac OSX files -.DS_Store diff --git a/packages/elements/.npmignore b/packages/elements/.npmignore deleted file mode 100644 index 1e8fc102c3d..00000000000 --- a/packages/elements/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -examples -dist/metafile-*.json diff --git a/packages/elements/CHANGELOG.md b/packages/elements/CHANGELOG.md deleted file mode 100644 index d9ae77c15c7..00000000000 --- a/packages/elements/CHANGELOG.md +++ /dev/null @@ -1,2284 +0,0 @@ -# @clerk/elements - -## 0.23.89 - -### Patch Changes - -- Updated dependencies [[`2a508d3`](https://github.com/clerk/javascript/commit/2a508d300561dfdf3471719d82ad1dd1f503d5b1), [`0307ea8`](https://github.com/clerk/javascript/commit/0307ea8e65831f0568b759413eb582346a99bd46), [`b117ebc`](https://github.com/clerk/javascript/commit/b117ebc956e1a5d48d5fdb7210de3344a74a524a)]: - - @clerk/clerk-react@5.58.1 - - @clerk/shared@3.39.0 - - @clerk/types@4.101.6 - -## 0.23.88 - -### Patch Changes - -- Updated dependencies [[`e31f3d5`](https://github.com/clerk/javascript/commit/e31f3d567302f99d8d073ba75cd934fb3c1eca7f), [`8376789`](https://github.com/clerk/javascript/commit/8376789de2383b52fabc563a9382622627055ecd), [`f917d68`](https://github.com/clerk/javascript/commit/f917d68fc2fc5d317770491e9d4d7185e1985d04), [`818c25a`](https://github.com/clerk/javascript/commit/818c25a9eec256245152725c64419c73e762c1a2), [`b41c0d5`](https://github.com/clerk/javascript/commit/b41c0d539835a5a43d15e3399bac7cbf046d9345)]: - - @clerk/shared@3.38.0 - - @clerk/clerk-react@5.58.0 - - @clerk/types@4.101.5 - -## 0.23.87 - -### Patch Changes - -- Updated dependencies [[`40a841d`](https://github.com/clerk/javascript/commit/40a841d56cd8983dce21376c832f1085c43a9518), [`f364924`](https://github.com/clerk/javascript/commit/f364924708f20f0bc7b8b291ea2ae01ce09e2e9f), [`f115e56`](https://github.com/clerk/javascript/commit/f115e56d14b5c49f52b6aca01b434dbe4f6193cf), [`cf66d07`](https://github.com/clerk/javascript/commit/cf66d07d48fec30a052831c43e3c38feef0597cc), [`d4aef71`](https://github.com/clerk/javascript/commit/d4aef71961d6d0abf8f1d1142c4e3ae943181c4b), [`3f99742`](https://github.com/clerk/javascript/commit/3f997427e400248502b0977e1b69e109574dfe7d), [`02798f5`](https://github.com/clerk/javascript/commit/02798f571065d8142cf1dade57b42b3e8ce0f818), [`07a30ce`](https://github.com/clerk/javascript/commit/07a30ce52b7d2ba85ce3533879700b9ec129152e), [`ce8b914`](https://github.com/clerk/javascript/commit/ce8b9149bff27866cdb686f1ab0b56cef8d8c697)]: - - @clerk/shared@3.37.0 - - @clerk/clerk-react@5.57.1 - - @clerk/types@4.101.4 - -## 0.23.86 - -### Patch Changes - -- Updated dependencies [[`f85abda`](https://github.com/clerk/javascript/commit/f85abdac03fde4a5109f31931c55b56a365aa748), [`36e43cc`](https://github.com/clerk/javascript/commit/36e43cc614865e52eefbd609a9491c32371cda44)]: - - @clerk/shared@3.36.0 - - @clerk/clerk-react@5.57.0 - - @clerk/types@4.101.3 - -## 0.23.85 - -### Patch Changes - -- Update peerDependencies to allow Next.js v16 ([#7274](https://github.com/clerk/javascript/pull/7274)) by [@nikosdouvlis](https://github.com/nikosdouvlis) - -- Updated dependencies [[`d8f59a6`](https://github.com/clerk/javascript/commit/d8f59a66d56d8fb0dfea353ecd86af97d0ec56b7)]: - - @clerk/shared@3.35.2 - - @clerk/clerk-react@5.56.2 - - @clerk/types@4.101.2 - -## 0.23.84 - -### Patch Changes - -- Updated dependencies [[`a9c13ca`](https://github.com/clerk/javascript/commit/a9c13cae5a6f46ca753d530878f7e4492ca7938b)]: - - @clerk/shared@3.35.1 - - @clerk/clerk-react@5.56.1 - - @clerk/types@4.101.1 - -## 0.23.83 - -### Patch Changes - -- Updated dependencies [[`7be8f45`](https://github.com/clerk/javascript/commit/7be8f458367b2c050b0dc8c0481d7bbe090ea400), [`bdbb0d9`](https://github.com/clerk/javascript/commit/bdbb0d91712a84fc214c534fc47b62b1a2028ac9), [`aa184a4`](https://github.com/clerk/javascript/commit/aa184a46a91f9dec3fd275ec5867a8366d310469), [`1d4e7a7`](https://github.com/clerk/javascript/commit/1d4e7a7769e9efaaa945e4ba6468ad47bd24c807), [`42f0d95`](https://github.com/clerk/javascript/commit/42f0d95e943d82960de3f7e5da17d199eff9fddd), [`c63cc8e`](https://github.com/clerk/javascript/commit/c63cc8e9c38ed0521a22ebab43e10111f04f9daf), [`d32d724`](https://github.com/clerk/javascript/commit/d32d724c34a921a176eca159273f270c2af4e787), [`00291bc`](https://github.com/clerk/javascript/commit/00291bc8ae03c06f7154bd937628e8193f6e3ce9)]: - - @clerk/shared@3.35.0 - - @clerk/clerk-react@5.56.0 - - @clerk/types@4.101.0 - -## 0.23.82 - -### Patch Changes - -- Updated dependencies [[`b5a7e2f`](https://github.com/clerk/javascript/commit/b5a7e2f8af5514e19e06918632d982be65f4a854), [`a1d10fc`](https://github.com/clerk/javascript/commit/a1d10fc6e231f27ec7eabd0db45b8f7e8c98250e), [`b944ff3`](https://github.com/clerk/javascript/commit/b944ff30494a8275450ca0d5129cdf58f02bea81), [`4011c5e`](https://github.com/clerk/javascript/commit/4011c5e0014ede5e480074b73d064a1bc2a577dd), [`791e942`](https://github.com/clerk/javascript/commit/791e9426181f89012d4c5380a99141f3bb7ae88a)]: - - @clerk/types@4.100.0 - - @clerk/shared@3.34.0 - - @clerk/clerk-react@5.55.0 - -## 0.23.81 - -### Patch Changes - -- Updated dependencies [[`613cb97`](https://github.com/clerk/javascript/commit/613cb97cb7b3b33c3865cfe008ef9b1ea624cc8d)]: - - @clerk/shared@3.33.0 - - @clerk/clerk-react@5.54.0 - - @clerk/types@4.99.0 - -## 0.23.80 - -### Patch Changes - -- Updated dependencies [[`cc11472`](https://github.com/clerk/javascript/commit/cc11472e7318b806ee43d609cd03fb0446f56146), [`539fad7`](https://github.com/clerk/javascript/commit/539fad7b80ed284a7add6cf8c4c45cf4c6a0a8b2), [`296fb0b`](https://github.com/clerk/javascript/commit/296fb0b8f34aca4f527508a5e6a6bbaad89cfdaa), [`c413433`](https://github.com/clerk/javascript/commit/c413433fee49701f252df574ce6a009d256c0cb9), [`a940c39`](https://github.com/clerk/javascript/commit/a940c39354bd0ee48d2fc9b0f3217ec20b2f32b4)]: - - @clerk/shared@3.32.0 - - @clerk/types@4.98.0 - - @clerk/clerk-react@5.53.9 - -## 0.23.79 - -### Patch Changes - -- Updated dependencies [[`92fba5d`](https://github.com/clerk/javascript/commit/92fba5d2874bf8a740f21ab0a4e21e63beb099f9)]: - - @clerk/clerk-react@5.53.8 - -## 0.23.78 - -### Patch Changes - -- Updated dependencies [[`a474c59`](https://github.com/clerk/javascript/commit/a474c59e3017358186de15c5b1e5b83002e72527), [`5536429`](https://github.com/clerk/javascript/commit/55364291e245ff05ca1e50e614e502d2081b87fb)]: - - @clerk/shared@3.31.1 - - @clerk/clerk-react@5.53.7 - - @clerk/types@4.97.2 - -## 0.23.77 - -### Patch Changes - -- Updated dependencies [[`85b5acc`](https://github.com/clerk/javascript/commit/85b5acc5ba192a8247f072fa93d5bc7d42986293), [`ea65d39`](https://github.com/clerk/javascript/commit/ea65d390cd6d3b0fdd35202492e858f8c8370f73), [`b09b29e`](https://github.com/clerk/javascript/commit/b09b29e82323c8fc508c49ffe10c77a737ef0bec)]: - - @clerk/types@4.97.1 - - @clerk/shared@3.31.0 - - @clerk/clerk-react@5.53.6 - -## 0.23.76 - -### Patch Changes - -- Deprecate `@clerk/types` in favor of `@clerk/shared/types` ([#7022](https://github.com/clerk/javascript/pull/7022)) by [@nikosdouvlis](https://github.com/nikosdouvlis) - - The `@clerk/types` package is now deprecated. All type definitions have been consolidated and moved to `@clerk/shared/types` to improve consistency across the Clerk ecosystem. - - **Backward Compatibility:** - - The `@clerk/types` package will remain available and will continue to re-export all types from `@clerk/shared/types` to ensure backward compatibility. Existing applications will continue to work without any immediate breaking changes. However, we strongly recommend migrating to `@clerk/shared/types` as new type definitions and updates will only be added to `@clerk/shared/types` starting with the next major release. - - **Migration Steps:** - - Please update your imports from `@clerk/types` to `@clerk/shared/types`: - - ```typescript - // Before - import type { ClerkResource, UserResource } from '@clerk/types'; - - // After - import type { ClerkResource, UserResource } from '@clerk/shared/types'; - ``` - - **What Changed:** - - All type definitions including: - - Resource types (User, Organization, Session, etc.) - - API response types - - Configuration types - - Authentication types - - Error types - - And all other shared types - - Have been moved from `packages/types/src` to `packages/shared/src/types` and are now exported via `@clerk/shared/types`. - -- Updated dependencies [[`3e0ef92`](https://github.com/clerk/javascript/commit/3e0ef9281194714f56dcf656d0caf4f75dcf097c), [`2587aa6`](https://github.com/clerk/javascript/commit/2587aa671dac1ca66711889bf1cd1c2e2ac8d7c8)]: - - @clerk/shared@3.30.0 - - @clerk/types@4.97.0 - - @clerk/clerk-react@5.53.5 - -## 0.23.75 - -### Patch Changes - -- Updated dependencies [[`791ff19`](https://github.com/clerk/javascript/commit/791ff19a55ecb39eac20e1533a7d578a30386388), [`439427e`](https://github.com/clerk/javascript/commit/439427e44adef4f43e5f0719adf5654ea58c33e7), [`7dfbf3a`](https://github.com/clerk/javascript/commit/7dfbf3aa1b5269aee2d3af628b02027be9767088), [`d33b7b5`](https://github.com/clerk/javascript/commit/d33b7b5538e9bcbbca1ac23c46793d0cddcef533)]: - - @clerk/shared@3.29.0 - - @clerk/types@4.96.0 - - @clerk/clerk-react@5.53.4 - -## 0.23.74 - -### Patch Changes - -- Updated dependencies [[`4d46e4e`](https://github.com/clerk/javascript/commit/4d46e4e601a5f2a213f1718af3f9271db4db0911)]: - - @clerk/types@4.95.1 - - @clerk/clerk-react@5.53.3 - - @clerk/shared@3.28.3 - -## 0.23.73 - -### Patch Changes - -- Updated dependencies [[`a172d51`](https://github.com/clerk/javascript/commit/a172d51df2d7f2e450c983a15ae897624304a764), [`947d0f5`](https://github.com/clerk/javascript/commit/947d0f5480b0151a392966cad2e1a45423f66035)]: - - @clerk/types@4.95.0 - - @clerk/shared@3.28.2 - - @clerk/clerk-react@5.53.2 - -## 0.23.72 - -### Patch Changes - -- Updated dependencies [[`d8147fb`](https://github.com/clerk/javascript/commit/d8147fb58bfd6caf9a4f0a36fdc48c630d00387f)]: - - @clerk/shared@3.28.1 - - @clerk/clerk-react@5.53.1 - -## 0.23.71 - -### Patch Changes - -- Updated dependencies [[`305f4ee`](https://github.com/clerk/javascript/commit/305f4eeb825086d55d1b0df198a0c43da8d94993), [`53214f9`](https://github.com/clerk/javascript/commit/53214f9a600074affc84d616bbbe7a6b625e7d33), [`1441e68`](https://github.com/clerk/javascript/commit/1441e6851102e9eed5697ad78c695f75b4a20db2), [`1236c74`](https://github.com/clerk/javascript/commit/1236c745fd58020e0972938ca0a9ae697a24af02)]: - - @clerk/shared@3.28.0 - - @clerk/types@4.94.0 - - @clerk/clerk-react@5.53.0 - -## 0.23.70 - -### Patch Changes - -- Updated dependencies [[`65b7cc7`](https://github.com/clerk/javascript/commit/65b7cc787a5f02a302b665b6eaf4d4b9a1cae4b0), [`6e09786`](https://github.com/clerk/javascript/commit/6e09786adeb0f481ca8b6d060ae8754b556a3f9a), [`aa7210c`](https://github.com/clerk/javascript/commit/aa7210c7fff34f6c6e2d4ca3cb736bbd35439cb6), [`2cd53cd`](https://github.com/clerk/javascript/commit/2cd53cd8c713dfa7f2e802fe08986411587095fa), [`1a2eee6`](https://github.com/clerk/javascript/commit/1a2eee6b8b6ead2d0481e93104fcaed6452bd1b9), [`2cd53cd`](https://github.com/clerk/javascript/commit/2cd53cd8c713dfa7f2e802fe08986411587095fa), [`1a2430a`](https://github.com/clerk/javascript/commit/1a2430a166fb1df5fbca76437c63423b18a49ced), [`31a04fc`](https://github.com/clerk/javascript/commit/31a04fc2b783f01cd4848c1e681af3b30e57bb2f), [`9766c4a`](https://github.com/clerk/javascript/commit/9766c4afd26f2841d6f79dbdec2584ef8becd22f), [`22b8e49`](https://github.com/clerk/javascript/commit/22b8e49f9fb65d55ab737d11f1f57a25bf947511), [`a66357e`](https://github.com/clerk/javascript/commit/a66357e8a5928199aebde408ec7cfaac152c2c42), [`dacc1af`](https://github.com/clerk/javascript/commit/dacc1af22e1d1af0940b2d626b8a47d376c19342)]: - - @clerk/types@4.93.0 - - @clerk/clerk-react@5.52.0 - - @clerk/shared@3.27.4 - -## 0.23.69 - -### Patch Changes - -- Updated dependencies [[`fba4781`](https://github.com/clerk/javascript/commit/fba4781ff2a2d16f8934029fa6fb77d70953f2be), [`a1f6714`](https://github.com/clerk/javascript/commit/a1f671480cda6f978db059ba0640d4ed8b08f112)]: - - @clerk/types@4.92.0 - - @clerk/clerk-react@5.51.0 - - @clerk/shared@3.27.3 - -## 0.23.68 - -### Patch Changes - -- Updated dependencies [[`f737d26`](https://github.com/clerk/javascript/commit/f737d268aa167889a4f3f7aba2658c2ba1fd909a), [`8777f35`](https://github.com/clerk/javascript/commit/8777f350f5fb51413609a53d9de05b2e5d1d7cfe), [`2c0128b`](https://github.com/clerk/javascript/commit/2c0128b05ecf48748f27f10f0b0215a279ba6cc1)]: - - @clerk/clerk-react@5.50.0 - - @clerk/types@4.91.0 - - @clerk/shared@3.27.2 - -## 0.23.67 - -### Patch Changes - -- Updated dependencies [[`37028ca`](https://github.com/clerk/javascript/commit/37028caad59cb0081ac74e70a44e4a419082a999)]: - - @clerk/types@4.90.0 - - @clerk/clerk-react@5.49.1 - - @clerk/shared@3.27.1 - -## 0.23.66 - -### Patch Changes - -- Updated dependencies [[`e3e77eb`](https://github.com/clerk/javascript/commit/e3e77eb277c6b36847265db7b863c418e3708ab6), [`9cf89cd`](https://github.com/clerk/javascript/commit/9cf89cd3402c278e8d5bfcd8277cee292bc45333), [`090ca74`](https://github.com/clerk/javascript/commit/090ca742c590bc4f369cf3e1ca2ec9917410ffe4), [`5546352`](https://github.com/clerk/javascript/commit/55463527df9a710ef3215c353bab1ef423d1de62)]: - - @clerk/shared@3.27.0 - - @clerk/clerk-react@5.49.0 - - @clerk/types@4.89.0 - -## 0.23.65 - -### Patch Changes - -- Updated dependencies [[`41e0a41`](https://github.com/clerk/javascript/commit/41e0a4190b33dd2c4bdc0d536bbe83fcf99af9b0), [`1aa9e9f`](https://github.com/clerk/javascript/commit/1aa9e9f10c051319e9ff4b1a0ecd71507bd6a6aa), [`a88ee58`](https://github.com/clerk/javascript/commit/a88ee5827adee0cc8a62246d03a3034d8566fe21), [`d6c7bbb`](https://github.com/clerk/javascript/commit/d6c7bbba23f38c0b3ca7edebb53028a05c7b38e6)]: - - @clerk/shared@3.26.1 - - @clerk/clerk-react@5.48.1 - - @clerk/types@4.88.0 - -## 0.23.64 - -### Patch Changes - -- Updated dependencies [[`bcf24f2`](https://github.com/clerk/javascript/commit/bcf24f2f91913fa0dd3fbf02b3bbef345c4e1ea9), [`1ceedad`](https://github.com/clerk/javascript/commit/1ceedad4bc5bc3d5f01c95185f82ff0f43983cf5), [`de90ede`](https://github.com/clerk/javascript/commit/de90ede82664b58bef9e294498384cf2c99a331e), [`9d4a95c`](https://github.com/clerk/javascript/commit/9d4a95c766396a0bc327fbf0560228bedb4828eb), [`428cd57`](https://github.com/clerk/javascript/commit/428cd57a8581a58a6a42325ec50eb98000068e97)]: - - @clerk/clerk-react@5.48.0 - - @clerk/types@4.87.0 - - @clerk/shared@3.26.0 - -## 0.23.63 - -### Patch Changes - -- Updated dependencies [[`23948dc`](https://github.com/clerk/javascript/commit/23948dc777ec6a17bafbae59c253a93143b0e105), [`82b84fe`](https://github.com/clerk/javascript/commit/82b84fed5f207673071ba7354a17f4a76e101201), [`54b4b5a`](https://github.com/clerk/javascript/commit/54b4b5a5f811f612fadf5c47ffda94a750c57a5e), [`50a8622`](https://github.com/clerk/javascript/commit/50a8622c3579306f15e5d40e5ea72b4fe4384ef7), [`23948dc`](https://github.com/clerk/javascript/commit/23948dc777ec6a17bafbae59c253a93143b0e105)]: - - @clerk/types@4.86.0 - - @clerk/shared@3.25.0 - - @clerk/clerk-react@5.47.0 - -## 0.23.62 - -### Patch Changes - -- Correctly specify dependency on `type-fest` package. ([#6711](https://github.com/clerk/javascript/pull/6711)) by [@dstaley](https://github.com/dstaley) - -- Updated dependencies [[`55490c3`](https://github.com/clerk/javascript/commit/55490c31fadc82bdca6cd5f2b22e5e158aaba0cb), [`e8d21de`](https://github.com/clerk/javascript/commit/e8d21de39b591973dad48fc1d1851c4d28b162fe), [`4a5bd7a`](https://github.com/clerk/javascript/commit/4a5bd7a4d9e96c89af07db69fc140ca2adb87f08), [`637f2e8`](https://github.com/clerk/javascript/commit/637f2e8768b76aaf756062b6b5b44bf651f66789)]: - - @clerk/types@4.85.0 - - @clerk/clerk-react@5.46.2 - - @clerk/shared@3.24.2 - -## 0.23.61 - -### Patch Changes - -- Updated dependencies [[`fced4fc`](https://github.com/clerk/javascript/commit/fced4fc869bb21c77826dfaf281b6640e0f0c006), [`9796fbf`](https://github.com/clerk/javascript/commit/9796fbf57494e108bdb531accc26bdb8e54e59f9), [`f28179b`](https://github.com/clerk/javascript/commit/f28179b30102550cc5601fcd22da7ca9a90959ee), [`e6e19d2`](https://github.com/clerk/javascript/commit/e6e19d2d2f3b2c4617b25f53830216a1d550e616), [`1b1e8b1`](https://github.com/clerk/javascript/commit/1b1e8b1fd33b787f956b17b193e5fd0a4cdc6cec)]: - - @clerk/types@4.84.1 - - @clerk/clerk-react@5.46.1 - - @clerk/shared@3.24.1 - -## 0.23.60 - -### Patch Changes - -- Updated dependencies [[`2a82737`](https://github.com/clerk/javascript/commit/2a8273705b9764e1a4613d5a0dbb738d0b156c05), [`cda5d7b`](https://github.com/clerk/javascript/commit/cda5d7b79b28dc03ec794ea54e0feb64b148cdd2), [`ba25a5b`](https://github.com/clerk/javascript/commit/ba25a5b5a3fa686a65f52e221d9d1712a389fea9), [`a50cfc8`](https://github.com/clerk/javascript/commit/a50cfc8f1dd168b436499e32fc8b0fc41d28bbff), [`377f67b`](https://github.com/clerk/javascript/commit/377f67b8e552d1a19efbe4530e9306675b7f8eab), [`65b12ee`](https://github.com/clerk/javascript/commit/65b12eeeb57ee80cdd8c36c5949d51f1227a413e), [`263722e`](https://github.com/clerk/javascript/commit/263722e61fd27403b4c8d9794880686771e123f9)]: - - @clerk/clerk-react@5.46.0 - - @clerk/types@4.84.0 - - @clerk/shared@3.24.0 - -## 0.23.59 - -### Patch Changes - -- Updated dependencies [[`600c648`](https://github.com/clerk/javascript/commit/600c648d4087a823341041c90018797fbc0033f0)]: - - @clerk/shared@3.23.0 - - @clerk/clerk-react@5.45.0 - - @clerk/types@4.83.0 - -## 0.23.58 - -### Patch Changes - -- Updated dependencies [[`d52714e`](https://github.com/clerk/javascript/commit/d52714e4cb7f369c74826cd4341c58eb1900abe4), [`ce49740`](https://github.com/clerk/javascript/commit/ce49740d474d6dd9da5096982ea4e9f14cf68f09), [`2ed539c`](https://github.com/clerk/javascript/commit/2ed539cc7f08ed4d70c33621563ad386ea8becc5), [`deaafe4`](https://github.com/clerk/javascript/commit/deaafe449773632d690aa2f8cafaf959392622b9), [`a26ecae`](https://github.com/clerk/javascript/commit/a26ecae09fd06cd34f094262f038a8eefbb23f7d), [`c16a7a5`](https://github.com/clerk/javascript/commit/c16a7a5837fc15e0e044baf9c809b8da6fbac795), [`05b6d65`](https://github.com/clerk/javascript/commit/05b6d65c0bc5736443325a5defee4c263ef196af)]: - - @clerk/clerk-react@5.44.0 - - @clerk/types@4.82.0 - - @clerk/shared@3.22.1 - -## 0.23.57 - -### Patch Changes - -- Updated dependencies [[`e52bf8e`](https://github.com/clerk/javascript/commit/e52bf8ebef74a9e123c69b69acde1340c01d32d7), [`c043c19`](https://github.com/clerk/javascript/commit/c043c1919854aaa5b9cf7f6df5bb517f5617f7a1), [`c28d29c`](https://github.com/clerk/javascript/commit/c28d29c79bb4f144d782313ca72df7db91a77340), [`172e054`](https://github.com/clerk/javascript/commit/172e054a3511be12d16ba19037db320c2d9838bf)]: - - @clerk/types@4.81.0 - - @clerk/clerk-react@5.43.1 - - @clerk/shared@3.22.0 - -## 0.23.56 - -### Patch Changes - -- Updated dependencies [[`8dc6bad`](https://github.com/clerk/javascript/commit/8dc6bad5c7051b59bd8c73e65d497f6a974bb1c3), [`aa6a3c3`](https://github.com/clerk/javascript/commit/aa6a3c3d3ba2de67a468c996cbf0bff43a09ddb8), [`db50c47`](https://github.com/clerk/javascript/commit/db50c4734920ada6002de8c62c994047eb6cb5a0)]: - - @clerk/types@4.80.0 - - @clerk/clerk-react@5.43.0 - - @clerk/shared@3.21.2 - -## 0.23.55 - -### Patch Changes - -- Updated dependencies [[`413468c`](https://github.com/clerk/javascript/commit/413468c9b9c8fb7576f8e4cbdccff98784e33fef), [`7b7eb1f`](https://github.com/clerk/javascript/commit/7b7eb1fc0235249c5c179239078294118f2947cd)]: - - @clerk/shared@3.21.1 - - @clerk/types@4.79.0 - - @clerk/clerk-react@5.42.2 - -## 0.23.54 - -### Patch Changes - -- Updated dependencies [[`5b24129`](https://github.com/clerk/javascript/commit/5b24129ddcfc2f7dc6eb79d8c818b4ff97c68e9a)]: - - @clerk/shared@3.21.0 - - @clerk/types@4.78.0 - - @clerk/clerk-react@5.42.1 - -## 0.23.53 - -### Patch Changes - -- Updated dependencies [[`4db1e58`](https://github.com/clerk/javascript/commit/4db1e58d70b60e1e236709b507666715d571e925), [`69498df`](https://github.com/clerk/javascript/commit/69498dfca3e6bb388eb8c94313eac06347dd5a27), [`59f1559`](https://github.com/clerk/javascript/commit/59f15593bab708b9e13eebfff6780c2d52b31b0a), [`69498df`](https://github.com/clerk/javascript/commit/69498dfca3e6bb388eb8c94313eac06347dd5a27)]: - - @clerk/types@4.77.0 - - @clerk/shared@3.20.1 - - @clerk/clerk-react@5.42.0 - -## 0.23.52 - -### Patch Changes - -- Updated dependencies [[`15fe106`](https://github.com/clerk/javascript/commit/15fe1060f730a6a4391f3d2451d23edd3218e1ae), [`173837c`](https://github.com/clerk/javascript/commit/173837c2526aa826b7981ee8d6d4f52c00675da5), [`8b52d7a`](https://github.com/clerk/javascript/commit/8b52d7ae19407e8ab5a5451bd7d34b6bc38417de), [`854dde8`](https://github.com/clerk/javascript/commit/854dde88e642c47b5a29ac8f576c8c1976e5d067), [`ae2e2d6`](https://github.com/clerk/javascript/commit/ae2e2d6b336be6b596cc855e549843beb5bfd2a1), [`037f25a`](https://github.com/clerk/javascript/commit/037f25a8171888168913b186b7edf871e0aaf197), [`f8b38b7`](https://github.com/clerk/javascript/commit/f8b38b7059e498fef3ac1271346be0710aa31c76)]: - - @clerk/types@4.76.0 - - @clerk/shared@3.20.0 - - @clerk/clerk-react@5.41.1 - -## 0.23.51 - -### Patch Changes - -- Updated dependencies [[`b72a3dd`](https://github.com/clerk/javascript/commit/b72a3dda2467720e5dc8cab3e7e6a110f3beb79b), [`d93b0ed`](https://github.com/clerk/javascript/commit/d93b0edf4adc57d48a26cb08444192887ccec659), [`6459f7d`](https://github.com/clerk/javascript/commit/6459f7dabe5f163f48ed73106bb901d8187da3e2), [`0ff648a`](https://github.com/clerk/javascript/commit/0ff648aeac0e2f5481596a98c8046d9d58a7bf75), [`9084759`](https://github.com/clerk/javascript/commit/90847593300be605e1ee1c06dac147ce68b25dc7)]: - - @clerk/types@4.75.0 - - @clerk/clerk-react@5.41.0 - - @clerk/shared@3.19.0 - -## 0.23.50 - -### Patch Changes - -- Updated dependencies [[`1ad16da`](https://github.com/clerk/javascript/commit/1ad16daa49795a861ae277001831230580b6b9f4), [`4edef81`](https://github.com/clerk/javascript/commit/4edef81dd423a0471e3f579dd6b36094aa8546aa), [`696f8e1`](https://github.com/clerk/javascript/commit/696f8e11a3e5391e6b5a97d98e929b8973575b9a), [`f318d22`](https://github.com/clerk/javascript/commit/f318d22cf83caaef272bcf532561a03ca72575e7), [`1cc66ab`](https://github.com/clerk/javascript/commit/1cc66aba1c0adac24323876e4cc3d96be888b07b)]: - - @clerk/clerk-react@5.40.0 - - @clerk/types@4.74.0 - - @clerk/shared@3.18.1 - -## 0.23.49 - -### Patch Changes - -- Updated dependencies [[`9368daf`](https://github.com/clerk/javascript/commit/9368dafb119b5a8ec6a9d6d82270e72bab6d8f1e), [`f93965f`](https://github.com/clerk/javascript/commit/f93965f64c81030f9fcf9d1cc4e4984d30cd12ec), [`7b6dcee`](https://github.com/clerk/javascript/commit/7b6dceea5bfd7f1cc1bf24126aa715307e24ae7f), [`ef87617`](https://github.com/clerk/javascript/commit/ef87617ae1fd125c806a33bfcfdf09c885319fa8)]: - - @clerk/shared@3.18.0 - - @clerk/clerk-react@5.39.0 - - @clerk/types@4.73.0 - -## 0.23.48 - -### Patch Changes - -- Updated dependencies [[`7a46679`](https://github.com/clerk/javascript/commit/7a46679a004739a7f712097c5779e9f5c068722e), [`05cc5ec`](https://github.com/clerk/javascript/commit/05cc5ecd82ecdbcc9922d3286224737a81813be0), [`22c35ef`](https://github.com/clerk/javascript/commit/22c35efb59226df2efaa2891fa4775c13312f4c6), [`e8d816a`](https://github.com/clerk/javascript/commit/e8d816a3350e862c3e9e1d4f8c96c047a0a016a2), [`aa9f185`](https://github.com/clerk/javascript/commit/aa9f185e21b58f8a6e03ea44ce29ee09ad2477d9), [`af0e123`](https://github.com/clerk/javascript/commit/af0e12393c9412281626e20dafb1b3a15558f6d9), [`241bbbd`](https://github.com/clerk/javascript/commit/241bbbd5ad3915419fe222861a2eeb0132a294e0), [`3d1d871`](https://github.com/clerk/javascript/commit/3d1d8711405646cf3c2aabe99e08337a1028703a)]: - - @clerk/shared@3.17.0 - - @clerk/clerk-react@5.38.1 - - @clerk/types@4.72.0 - -## 0.23.47 - -### Patch Changes - -- Updated dependencies [[`e404456`](https://github.com/clerk/javascript/commit/e4044566bca81f63c8e9c630fdec0f498ad6fc08), [`2803133`](https://github.com/clerk/javascript/commit/28031330a9810946feb44b93be10c067fb3b63ba), [`f1d9d34`](https://github.com/clerk/javascript/commit/f1d9d3482a796dd5f7796ede14159850e022cba2), [`d58b959`](https://github.com/clerk/javascript/commit/d58b9594cf65158e87dbaa90d632c45f543373e1), [`f6375f0`](https://github.com/clerk/javascript/commit/f6375f01e8d8a06e12d4a71285912e9dda7b6f20), [`822ba1f`](https://github.com/clerk/javascript/commit/822ba1fd5e7daf665120cf183e4600a227098d53), [`d4d2612`](https://github.com/clerk/javascript/commit/d4d2612483baf356c389ef0ba5084059025481f2)]: - - @clerk/types@4.71.0 - - @clerk/shared@3.16.0 - - @clerk/clerk-react@5.38.0 - -## 0.23.46 - -### Patch Changes - -- Updated dependencies [[`cfa7882`](https://github.com/clerk/javascript/commit/cfa78827cea6e81ce671ae204f529d2f93e3304d), [`b0fdc9e`](https://github.com/clerk/javascript/commit/b0fdc9eaf764ca0c17cbe0810b7d240f6d9db0b6)]: - - @clerk/clerk-react@5.37.0 - - @clerk/types@4.70.1 - - @clerk/shared@3.15.1 - -## 0.23.45 - -### Patch Changes - -- Updated dependencies [[`8feb59b`](https://github.com/clerk/javascript/commit/8feb59b808254a59c9bf4cf9c00f177e29e5e41b), [`cd59c0e`](https://github.com/clerk/javascript/commit/cd59c0e5512a341dd8fb420aca583333c8243aa5), [`cd59c0e`](https://github.com/clerk/javascript/commit/cd59c0e5512a341dd8fb420aca583333c8243aa5)]: - - @clerk/clerk-react@5.36.0 - - @clerk/types@4.70.0 - - @clerk/shared@3.15.0 - -## 0.23.44 - -### Patch Changes - -- Updated dependencies [[`fecc99d`](https://github.com/clerk/javascript/commit/fecc99d43cb7db5b99863829acb234cbce0da264), [`959d63d`](https://github.com/clerk/javascript/commit/959d63de27e5bfe27b46699b441dfd4e48616bf8), [`10e1060`](https://github.com/clerk/javascript/commit/10e10605b18a58f33a93caed058159c190678e74), [`92c44dd`](https://github.com/clerk/javascript/commit/92c44dd9d51e771a928a8da7004bdb8f8bdbaf58), [`a04a8f5`](https://github.com/clerk/javascript/commit/a04a8f5f81241ee41d93cd64793beca9d6296abb), [`c61855c`](https://github.com/clerk/javascript/commit/c61855c51d9c129d48c4543da3719939ad82f623), [`43ea069`](https://github.com/clerk/javascript/commit/43ea069c570dc64503fc82356ad28a2e43689d45)]: - - @clerk/clerk-react@5.35.4 - - @clerk/types@4.69.0 - - @clerk/shared@3.14.0 - -## 0.23.43 - -### Patch Changes - -- Updated dependencies [[`d2f6f9e`](https://github.com/clerk/javascript/commit/d2f6f9e02036a4288916fcce14f24be5d56561c4), [`a329836`](https://github.com/clerk/javascript/commit/a329836a6c64f0a551a277ccae07043456a70523), [`6041c39`](https://github.com/clerk/javascript/commit/6041c39a31e787a6065dbc3f21e1c569982a06de), [`3f1270d`](https://github.com/clerk/javascript/commit/3f1270db86a21ead0ed6f0bd4f9986485203e973)]: - - @clerk/clerk-react@5.35.3 - - @clerk/types@4.68.0 - - @clerk/shared@3.13.0 - -## 0.23.42 - -### Patch Changes - -- Updated dependencies [[`2a90b68`](https://github.com/clerk/javascript/commit/2a90b689550ae960496c9292ca23e0225e3425cd), [`af50905`](https://github.com/clerk/javascript/commit/af50905ea497ed3286c8c4c374498e06ca6ee82b)]: - - @clerk/clerk-react@5.35.2 - - @clerk/types@4.67.0 - - @clerk/shared@3.12.3 - -## 0.23.41 - -### Patch Changes - -- Updated dependencies [[`8ee859c`](https://github.com/clerk/javascript/commit/8ee859ce00d1d5747c14a80fe7166303e64a4f1f)]: - - @clerk/shared@3.12.2 - - @clerk/types@4.66.1 - - @clerk/clerk-react@5.35.1 - -## 0.23.40 - -### Patch Changes - -- Updated dependencies [[`025e304`](https://github.com/clerk/javascript/commit/025e304c4d6402dfd750ee51ac9c8fc2dea1f353), [`dedf487`](https://github.com/clerk/javascript/commit/dedf48703986d547d5b28155b0182a51030cffeb), [`b96114e`](https://github.com/clerk/javascript/commit/b96114e438638896ba536bb7a17b09cdadcd9407)]: - - @clerk/types@4.66.0 - - @clerk/clerk-react@5.35.0 - - @clerk/shared@3.12.1 - -## 0.23.39 - -### Patch Changes - -- Updated dependencies [[`2be6a53`](https://github.com/clerk/javascript/commit/2be6a53959cb8a3127c2eb5d1aeb4248872d2c24), [`f6a1c35`](https://github.com/clerk/javascript/commit/f6a1c35bd5fb4bd2a3cd45bdaf9defe6be59d4a9), [`6826d0b`](https://github.com/clerk/javascript/commit/6826d0bbd03e844d49224565878a4326684f06b4), [`f6a1c35`](https://github.com/clerk/javascript/commit/f6a1c35bd5fb4bd2a3cd45bdaf9defe6be59d4a9), [`97a07f7`](https://github.com/clerk/javascript/commit/97a07f78b4b0c3dc701a2610097ec7d6232f79e7)]: - - @clerk/types@4.65.0 - - @clerk/shared@3.12.0 - - @clerk/clerk-react@5.34.0 - -## 0.23.38 - -### Patch Changes - -- Updated dependencies [[`f42c4fe`](https://github.com/clerk/javascript/commit/f42c4fedfdab873129b876eba38b3677f190b460), [`ec207dc`](https://github.com/clerk/javascript/commit/ec207dcd2a13340cfa4e3b80d3d52d1b4e7d5f23), [`ec207dc`](https://github.com/clerk/javascript/commit/ec207dcd2a13340cfa4e3b80d3d52d1b4e7d5f23), [`0e0cc1f`](https://github.com/clerk/javascript/commit/0e0cc1fa85347d727a4fd3718fe45b0f0244ddd9)]: - - @clerk/types@4.64.0 - - @clerk/clerk-react@5.33.0 - - @clerk/shared@3.11.0 - -## 0.23.37 - -### Patch Changes - -- Updated dependencies [[`abd8446`](https://github.com/clerk/javascript/commit/abd844609dad263d974da7fbf5e3575afce73abe), [`8387a39`](https://github.com/clerk/javascript/commit/8387a392a04906f0f10d84c61cfee36f23942f85), [`f2a6641`](https://github.com/clerk/javascript/commit/f2a66419b1813abc86ea98fde7475861995a1486)]: - - @clerk/shared@3.10.2 - - @clerk/types@4.63.0 - - @clerk/clerk-react@5.32.4 - -## 0.23.36 - -### Patch Changes - -- Updated dependencies [[`02a1f42`](https://github.com/clerk/javascript/commit/02a1f42dfdb28ea956d6cbd3fbabe10093d2fad8), [`edc0bfd`](https://github.com/clerk/javascript/commit/edc0bfdae929dad78a99dfd6275aad947d9ddd73)]: - - @clerk/shared@3.10.1 - - @clerk/clerk-react@5.32.3 - - @clerk/types@4.62.1 - -## 0.23.35 - -### Patch Changes - -- Updated dependencies [[`f1be1fe`](https://github.com/clerk/javascript/commit/f1be1fe3d575c11acd04fc7aadcdec8f89829894), [`bffb42a`](https://github.com/clerk/javascript/commit/bffb42aaf266a188b9ae7d16ace3024d468a3bd4)]: - - @clerk/types@4.62.0 - - @clerk/shared@3.10.0 - - @clerk/clerk-react@5.32.2 - -## 0.23.34 - -### Patch Changes - -- Updated dependencies [[`b495279`](https://github.com/clerk/javascript/commit/b4952796e3c7dee4ab4726de63a17b7f4265ce37), [`c3fa15d`](https://github.com/clerk/javascript/commit/c3fa15d60642b4fcbcf26e21caaca0fc60975795), [`52d5e57`](https://github.com/clerk/javascript/commit/52d5e5768d54725b4d20d028135746493e05d44c), [`15a945c`](https://github.com/clerk/javascript/commit/15a945c02a9f6bc8d2f7d1e3534217100bf45936), [`72629b0`](https://github.com/clerk/javascript/commit/72629b06fb1fe720fa2a61462306a786a913e9a8)]: - - @clerk/types@4.61.0 - - @clerk/shared@3.9.8 - - @clerk/clerk-react@5.32.1 - -## 0.23.33 - -### Patch Changes - -- Updated dependencies [[`19e9e11`](https://github.com/clerk/javascript/commit/19e9e11af04f13fd12975fbf7016fe0583202056), [`18bcb64`](https://github.com/clerk/javascript/commit/18bcb64a3e8b6d352d7933ed094d68214e6e80fb), [`138f733`](https://github.com/clerk/javascript/commit/138f733f13121487268a4f96e6eb2cffedc6e238), [`18bcb64`](https://github.com/clerk/javascript/commit/18bcb64a3e8b6d352d7933ed094d68214e6e80fb), [`48be55b`](https://github.com/clerk/javascript/commit/48be55b61a86e014dd407414764d24bb43fd26f3), [`2c6f805`](https://github.com/clerk/javascript/commit/2c6f805a9e6e4685990f9a8abc740b2d0859a453), [`97749d5`](https://github.com/clerk/javascript/commit/97749d570bc687c7e05cd800a50e0ae4180a371d)]: - - @clerk/types@4.60.1 - - @clerk/clerk-react@5.32.0 - - @clerk/shared@3.9.7 - -## 0.23.32 - -### Patch Changes - -- Updated dependencies [[`d8fa5d9`](https://github.com/clerk/javascript/commit/d8fa5d9d3d8dc575260d8d2b7c7eeeb0052d0b0d), [`be2e89c`](https://github.com/clerk/javascript/commit/be2e89ca11aa43d48f74c57a5a34e20d85b4003c), [`5644d94`](https://github.com/clerk/javascript/commit/5644d94f711a0733e4970c3f15c24d56cafc8743), [`a3232c7`](https://github.com/clerk/javascript/commit/a3232c7ee8c1173d2ce70f8252fc083c7bf19374), [`b578225`](https://github.com/clerk/javascript/commit/b5782258242474c9b0987a3f8349836cd763f24b), [`8838120`](https://github.com/clerk/javascript/commit/8838120596830b88fec1c6c853371dabfec74a0d)]: - - @clerk/types@4.60.0 - - @clerk/clerk-react@5.31.9 - - @clerk/shared@3.9.6 - -## 0.23.31 - -### Patch Changes - -- Updated dependencies [[`f897773`](https://github.com/clerk/javascript/commit/f89777379da63cf45039c1570b51ba10a400817c), [`2c6a0cc`](https://github.com/clerk/javascript/commit/2c6a0cca6e824bafc6b0d0501784517a5b1f75ea), [`71e6a1f`](https://github.com/clerk/javascript/commit/71e6a1f1024d65b7a09cdc8fa81ce0164e0a34cb)]: - - @clerk/shared@3.9.5 - - @clerk/types@4.59.3 - - @clerk/clerk-react@5.31.8 - -## 0.23.30 - -### Patch Changes - -- Updated dependencies [[`6ed3dfc`](https://github.com/clerk/javascript/commit/6ed3dfc1bc742ac9d9a2307fe8e4733411cbc0d7)]: - - @clerk/types@4.59.2 - - @clerk/clerk-react@5.31.7 - - @clerk/shared@3.9.4 - -## 0.23.29 - -### Patch Changes - -- Updated dependencies [[`f237d76`](https://github.com/clerk/javascript/commit/f237d7617e5398ca0ba981e4336cac2191505b00)]: - - @clerk/shared@3.9.3 - - @clerk/clerk-react@5.31.6 - -## 0.23.28 - -### Patch Changes - -- Updated dependencies [[`c305b31`](https://github.com/clerk/javascript/commit/c305b310e351e9ce2012f805b35e464c3e43e310), [`6bb480e`](https://github.com/clerk/javascript/commit/6bb480ef663a6dfa219bc9546aca087d5d9624d0)]: - - @clerk/types@4.59.1 - - @clerk/shared@3.9.2 - - @clerk/clerk-react@5.31.5 - -## 0.23.27 - -### Patch Changes - -- Updated dependencies [[`b1337df`](https://github.com/clerk/javascript/commit/b1337dfeae8ccf8622efcf095e3201f9bbf1cefa), [`65f0878`](https://github.com/clerk/javascript/commit/65f08788ee5e56242eee2194c73ba90965c75c97), [`df6fefd`](https://github.com/clerk/javascript/commit/df6fefd05fd2df93f5286d97e546b48911adea7c), [`4282bfa`](https://github.com/clerk/javascript/commit/4282bfa09491225bde7d619fe9a3561062703f69), [`5491491`](https://github.com/clerk/javascript/commit/5491491711e0a8ee37828451c1f603a409de32cf)]: - - @clerk/types@4.59.0 - - @clerk/clerk-react@5.31.4 - - @clerk/shared@3.9.1 - -## 0.23.26 - -### Patch Changes - -- Updated dependencies [[`1ff6d6e`](https://github.com/clerk/javascript/commit/1ff6d6efbe838b3f7f6977b2b5215c2cafd715f6), [`fbf3cf4`](https://github.com/clerk/javascript/commit/fbf3cf4916469c4e118870bf12efca2d0f77d9d8)]: - - @clerk/shared@3.9.0 - - @clerk/types@4.58.1 - - @clerk/clerk-react@5.31.3 - -## 0.23.25 - -### Patch Changes - -- Updated dependencies [[`0f5145e`](https://github.com/clerk/javascript/commit/0f5145e164f3d3d5faf57e58162b05e7110d2403), [`afdfd18`](https://github.com/clerk/javascript/commit/afdfd18d645608dec37e52a291a91ba5f42dcbe7), [`b7c51ba`](https://github.com/clerk/javascript/commit/b7c51baac6df1129b468274c9a7f63ca303f16ce), [`437b53b`](https://github.com/clerk/javascript/commit/437b53b67e281d076b5b3f927e11c1d64666d154), [`5217155`](https://github.com/clerk/javascript/commit/52171554250c5c58f4f497b6d3c7416e79ac77da)]: - - @clerk/types@4.58.0 - - @clerk/clerk-react@5.31.2 - - @clerk/shared@3.8.2 - -## 0.23.24 - -### Patch Changes - -- Updated dependencies [[`4db96e0`](https://github.com/clerk/javascript/commit/4db96e0ff2ab44c7bdd8540e09ec70b84b19d3eb), [`36fb43f`](https://github.com/clerk/javascript/commit/36fb43f8b35866bdc20680fac58020f036d30d1f), [`e5ac444`](https://github.com/clerk/javascript/commit/e5ac4447f52bb6887ad686feab308fe9daf76e33), [`4db96e0`](https://github.com/clerk/javascript/commit/4db96e0ff2ab44c7bdd8540e09ec70b84b19d3eb), [`d227805`](https://github.com/clerk/javascript/commit/d22780599a5e29545a3d8309cc411c2e8659beac)]: - - @clerk/types@4.57.1 - - @clerk/clerk-react@5.31.1 - - @clerk/shared@3.8.1 - -## 0.23.23 - -### Patch Changes - -- Updated dependencies [[`db0138f`](https://github.com/clerk/javascript/commit/db0138f3f72aea8cb68a5684a90123f733848f63), [`45e8298`](https://github.com/clerk/javascript/commit/45e829890ec9ac66f07e0d7076cd283f14c893ed), [`aa97231`](https://github.com/clerk/javascript/commit/aa97231962e3f472a46135e376159c6ddcf1157b), [`c792f37`](https://github.com/clerk/javascript/commit/c792f37129fd6475d5af95146e9ef0f1c8eff730), [`3bf08a9`](https://github.com/clerk/javascript/commit/3bf08a9e0a9e65496edac5fc3bb22ad7b561df26), [`74cf3b2`](https://github.com/clerk/javascript/commit/74cf3b28cdf622a942aaf99caabfba74b7e856fd), [`037b113`](https://github.com/clerk/javascript/commit/037b113aaedd53d4647d88f1659eb9c14cf6f275), [`c15a412`](https://github.com/clerk/javascript/commit/c15a412169058e2304a51c9e92ffaa7f6bb2a898), [`7726a03`](https://github.com/clerk/javascript/commit/7726a03a7fec4d292b6de2587b84ed4371984c23), [`ed10566`](https://github.com/clerk/javascript/commit/ed1056637624eec5bfd50333407c1e63e34c193b), [`b846a9a`](https://github.com/clerk/javascript/commit/b846a9ab96db6b1d8344a4b693051618865508a8), [`e66c800`](https://github.com/clerk/javascript/commit/e66c8002b82b2902f77e852e16482f5cfb062d2c), [`45e8298`](https://github.com/clerk/javascript/commit/45e829890ec9ac66f07e0d7076cd283f14c893ed), [`9c41091`](https://github.com/clerk/javascript/commit/9c41091eb795bce8ffeeeca0264ae841fe07b426), [`29462b4`](https://github.com/clerk/javascript/commit/29462b433eb411ce614e4768e5844cacd00c1975), [`322c43f`](https://github.com/clerk/javascript/commit/322c43f6807a932c3cfaaef1b587b472c80180d2), [`17397f9`](https://github.com/clerk/javascript/commit/17397f95b715bd4fefd7f63c1d351abcf1c8ee16), [`45e8298`](https://github.com/clerk/javascript/commit/45e829890ec9ac66f07e0d7076cd283f14c893ed)]: - - @clerk/types@4.57.0 - - @clerk/clerk-react@5.31.0 - - @clerk/shared@3.8.0 - -## 0.23.22 - -### Patch Changes - -- Updated dependencies [[`9ec0a73`](https://github.com/clerk/javascript/commit/9ec0a7353e9f6ea661c3d7b9542423b6eb1d29e9), [`d9222fc`](https://github.com/clerk/javascript/commit/d9222fc3c21da2bcae30b06f0b1897f526935582)]: - - @clerk/types@4.56.3 - - @clerk/clerk-react@5.30.4 - - @clerk/shared@3.7.8 - -## 0.23.21 - -### Patch Changes - -- Updated dependencies [[`225b9ca`](https://github.com/clerk/javascript/commit/225b9ca21aba44930872a85d6b112ee2a1b606b9)]: - - @clerk/types@4.56.2 - - @clerk/clerk-react@5.30.3 - - @clerk/shared@3.7.7 - -## 0.23.20 - -### Patch Changes - -- Updated dependencies [[`387bf62`](https://github.com/clerk/javascript/commit/387bf623406306e0c5c08da937f4930a7ec5e4a5), [`2716622`](https://github.com/clerk/javascript/commit/27166224e12af582298460d438bd7f83ea8e04bf), [`294da82`](https://github.com/clerk/javascript/commit/294da82336e7a345900d7ef9b28f56a7c8864c52)]: - - @clerk/types@4.56.1 - - @clerk/shared@3.7.6 - - @clerk/clerk-react@5.30.2 - -## 0.23.19 - -### Patch Changes - -- Updated dependencies [[`b02e766`](https://github.com/clerk/javascript/commit/b02e76627e47aec314573586451fa345a089115a), [`5d78b28`](https://github.com/clerk/javascript/commit/5d78b286b63e35fbcf44aac1f7657cbeaba4d659), [`d7f4438`](https://github.com/clerk/javascript/commit/d7f4438fa4bfd04474d5cdb9212ba908568ad6d2), [`5866855`](https://github.com/clerk/javascript/commit/58668550ec91d5511cf775972c54dc485185cc58), [`0007106`](https://github.com/clerk/javascript/commit/00071065998a3676c51e396b4c0afcbf930a9898), [`462b5b2`](https://github.com/clerk/javascript/commit/462b5b271d4e120d58a85818a358b60a6b3c8100), [`447d7a9`](https://github.com/clerk/javascript/commit/447d7a9e133c2a0e7db014bd5837e6ffff08f572), [`2beea29`](https://github.com/clerk/javascript/commit/2beea2957c67bc62446fe24d36332b0a4e850d7d), [`115601d`](https://github.com/clerk/javascript/commit/115601d12fd65dbf3011c0cda368525a2b95bfeb)]: - - @clerk/types@4.56.0 - - @clerk/clerk-react@5.30.1 - - @clerk/shared@3.7.5 - -## 0.23.18 - -### Patch Changes - -- Updated dependencies [[`8b25035`](https://github.com/clerk/javascript/commit/8b25035aa49382fe1cd1c6f30ec80e86bcf9d66e), [`f0f1ed7`](https://github.com/clerk/javascript/commit/f0f1ed7ef3666bfc1cbf945326e94a51e83c4646), [`25c3502`](https://github.com/clerk/javascript/commit/25c35023ee995acbf8f5c8989619ebc176a366d6)]: - - @clerk/types@4.55.1 - - @clerk/clerk-react@5.30.0 - - @clerk/shared@3.7.4 - -## 0.23.17 - -### Patch Changes - -- Updated dependencies [[`4334598`](https://github.com/clerk/javascript/commit/4334598108ff2cfa3c25b5a46117c1c9c65b7974), [`33201bf`](https://github.com/clerk/javascript/commit/33201bf972d6a980617d47ebd776bef76f871833), [`4334598`](https://github.com/clerk/javascript/commit/4334598108ff2cfa3c25b5a46117c1c9c65b7974), [`0ae0403`](https://github.com/clerk/javascript/commit/0ae040303d239b75a3221436354a2c2ecdb85aae)]: - - @clerk/clerk-react@5.29.0 - - @clerk/types@4.55.0 - - @clerk/shared@3.7.3 - -## 0.23.16 - -### Patch Changes - -- Updated dependencies [[`45486ac`](https://github.com/clerk/javascript/commit/45486acebf4d133efb09a3622a738cdbf4e51d66), [`837692a`](https://github.com/clerk/javascript/commit/837692aa40197b1574783ad36d0d017a771c08e1), [`0c00e59`](https://github.com/clerk/javascript/commit/0c00e59ff4714491650ac9480ae3b327c626d30d), [`6a5f644`](https://github.com/clerk/javascript/commit/6a5f6447a36a635d6201f8bb7619fb844ab21b79)]: - - @clerk/types@4.54.2 - - @clerk/shared@3.7.2 - - @clerk/clerk-react@5.28.2 - -## 0.23.15 - -### Patch Changes - -- Updated dependencies [[`ab939fd`](https://github.com/clerk/javascript/commit/ab939fdb29150c376280b42f861a188a33f57dcc), [`03284da`](https://github.com/clerk/javascript/commit/03284da6a93a790ce3e3ebbd871c06e19f5a8803), [`7389ba3`](https://github.com/clerk/javascript/commit/7389ba3164ca0d848fb0a9de5d7e9716925fadcc), [`f6ef841`](https://github.com/clerk/javascript/commit/f6ef841125ff21ca8cae731d1f47f3a101d887e1), [`e634830`](https://github.com/clerk/javascript/commit/e6348301ab56a7868f24c1b9a4dd9e1d60f6027b), [`f8887b2`](https://github.com/clerk/javascript/commit/f8887b2cbd145e8e49bec890e8b6e02e34178d6a)]: - - @clerk/types@4.54.1 - - @clerk/shared@3.7.1 - - @clerk/clerk-react@5.28.1 - -## 0.23.14 - -### Patch Changes - -- Updated dependencies [[`e4d04ae`](https://github.com/clerk/javascript/commit/e4d04aea490ab67e3431729398d3f4c46fc3e7e7), [`431a821`](https://github.com/clerk/javascript/commit/431a821b590835bcf6193a4cbdd234c5e763e08c), [`93068ea`](https://github.com/clerk/javascript/commit/93068ea9eb19d8c8b9c7ade35d0cd860e08049fc), [`431a821`](https://github.com/clerk/javascript/commit/431a821b590835bcf6193a4cbdd234c5e763e08c), [`431a821`](https://github.com/clerk/javascript/commit/431a821b590835bcf6193a4cbdd234c5e763e08c), [`103bc03`](https://github.com/clerk/javascript/commit/103bc03571c8845df205f4c6fd0c871c3368d1d0), [`431a821`](https://github.com/clerk/javascript/commit/431a821b590835bcf6193a4cbdd234c5e763e08c), [`48438b4`](https://github.com/clerk/javascript/commit/48438b409036088701bda7e1e732d6a51bee8cdc), [`196dcb4`](https://github.com/clerk/javascript/commit/196dcb47928bd22a3382197f8594a590f688faee)]: - - @clerk/types@4.54.0 - - @clerk/shared@3.7.0 - - @clerk/clerk-react@5.28.0 - -## 0.23.13 - -### Patch Changes - -- Improve JSDoc comments ([#5575](https://github.com/clerk/javascript/pull/5575)) by [@LekoArts](https://github.com/LekoArts) - -- Upgrading xstate and @xstate/react to add react@19 as a transitive peerDependency ([#5572](https://github.com/clerk/javascript/pull/5572)) by [@jacekradko](https://github.com/jacekradko) - -- Updated dependencies [[`70c9db9`](https://github.com/clerk/javascript/commit/70c9db9f3b51ba034f76e0cc4cf338e7b406d9b1), [`554242e`](https://github.com/clerk/javascript/commit/554242e16e50c92a6afb6ed74c681b04b9f113b5), [`cc1f9a0`](https://github.com/clerk/javascript/commit/cc1f9a0adb7771b615b0f2994a5ac571b59889dd), [`8186cb5`](https://github.com/clerk/javascript/commit/8186cb564575ac3ce97079ec203865bf5deb05ee)]: - - @clerk/shared@3.6.0 - - @clerk/clerk-react@5.27.0 - - @clerk/types@4.53.0 - -## 0.23.12 - -### Patch Changes - -- Updated dependencies [[`3ad3bc8`](https://github.com/clerk/javascript/commit/3ad3bc8380b354b0cd952eb58eb6c07650efa0f2), [`cfa94b8`](https://github.com/clerk/javascript/commit/cfa94b88476608edf8c2486e8ec0d3f3f82e0bfb), [`2033919`](https://github.com/clerk/javascript/commit/203391964857b98dae11944799d1e6328439e838), [`5f3cc46`](https://github.com/clerk/javascript/commit/5f3cc460b6b775b5a74746758b8cff11649a877a)]: - - @clerk/shared@3.5.0 - - @clerk/types@4.52.0 - - @clerk/clerk-react@5.26.2 - -## 0.23.11 - -### Patch Changes - -- Updated dependencies [[`f6f275d`](https://github.com/clerk/javascript/commit/f6f275dac5ae83ac0c2016a85a6a0cee9513f224)]: - - @clerk/types@4.51.1 - - @clerk/clerk-react@5.26.1 - - @clerk/shared@3.4.1 - -## 0.23.10 - -### Patch Changes - -- Updated dependencies [[`e1ec52b`](https://github.com/clerk/javascript/commit/e1ec52b93038c9cb24e030dc06e53825a384a480), [`bebb6d8`](https://github.com/clerk/javascript/commit/bebb6d8af66b2bb7a4b3bdf96f9d480e65b31ba2), [`a8180ce`](https://github.com/clerk/javascript/commit/a8180ceef447a13d84eac6a64ed7a04d791d8d64), [`d0d5203`](https://github.com/clerk/javascript/commit/d0d5203e4ee9e2e1bed5c00ef0f87f0130f1d298), [`6112420`](https://github.com/clerk/javascript/commit/6112420889f1577fb16d7bfa706aaffe1090093d), [`026ad57`](https://github.com/clerk/javascript/commit/026ad5717cf661182c219fb0ffab4522083065c3), [`9b25e31`](https://github.com/clerk/javascript/commit/9b25e311cf5e15f896c7948faa42ace45df364c5)]: - - @clerk/clerk-react@5.26.0 - - @clerk/types@4.51.0 - - @clerk/shared@3.4.0 - -## 0.23.9 - -### Patch Changes - -- Updated dependencies [[`60a9a51`](https://github.com/clerk/javascript/commit/60a9a51dff7d59e7397536586cf1cfe029bc021b), [`e984494`](https://github.com/clerk/javascript/commit/e984494416dda9a6f04acaaba61f8c2683090961), [`ec4521b`](https://github.com/clerk/javascript/commit/ec4521b4fe56602f524a0c6d1b09d21aef5d8bd0), [`38828ae`](https://github.com/clerk/javascript/commit/38828ae58d6d4e8e3c60945284930179b2b6bb40), [`f30fa75`](https://github.com/clerk/javascript/commit/f30fa750754f19030f932a666d2bdbdf0d86743d), [`9c68678`](https://github.com/clerk/javascript/commit/9c68678e87047e6312b708b775ebfb23a3e22f8a), [`619cde8`](https://github.com/clerk/javascript/commit/619cde8c532d635d910ebbc08ad6abcc025694b4)]: - - @clerk/shared@3.3.0 - - @clerk/clerk-react@5.25.6 - - @clerk/types@4.50.2 - -## 0.23.8 - -### Patch Changes - -- Updated dependencies [[`e20fb6b`](https://github.com/clerk/javascript/commit/e20fb6b397fb69c9d5af4e321267b82f12a5f127), [`77e6462`](https://github.com/clerk/javascript/commit/77e64628560cab688af214edb5922e67cd68a951)]: - - @clerk/shared@3.2.3 - - @clerk/types@4.50.1 - - @clerk/clerk-react@5.25.5 - -## 0.23.7 - -### Patch Changes - -- Updated dependencies [[`1da28a2`](https://github.com/clerk/javascript/commit/1da28a28bf602069b433c15b92df21f682779294), [`a9b618d`](https://github.com/clerk/javascript/commit/a9b618dfa97a0dacc462186c8b2588ad5ddb6902), [`f20dc15`](https://github.com/clerk/javascript/commit/f20dc159f542449e7f5b437b70d3eb3ba04d6975), [`4d9f1ee`](https://github.com/clerk/javascript/commit/4d9f1ee8c22fe1e4a166ff054d0af4d37b829f0a)]: - - @clerk/types@4.50.0 - - @clerk/shared@3.2.2 - - @clerk/clerk-react@5.25.4 - -## 0.23.6 - -### Patch Changes - -- Updated dependencies [[`466ed13`](https://github.com/clerk/javascript/commit/466ed136af73b59b267d92ad3296039d1c3a4fcc)]: - - @clerk/types@4.49.2 - - @clerk/clerk-react@5.25.3 - - @clerk/shared@3.2.1 - -## 0.23.5 - -### Patch Changes - -- Updated dependencies [[`892bc0e`](https://github.com/clerk/javascript/commit/892bc0eee9e0bb04d327eb84b44201fa34806483)]: - - @clerk/shared@3.2.0 - - @clerk/clerk-react@5.25.2 - -## 0.23.4 - -### Patch Changes - -- Updated dependencies [[`e513333`](https://github.com/clerk/javascript/commit/e5133330a196c5c3742634cc9c3d3233ff488b0d), [`3910ebe`](https://github.com/clerk/javascript/commit/3910ebea85817273f18fd2f3f142dd1c728e2220), [`e513333`](https://github.com/clerk/javascript/commit/e5133330a196c5c3742634cc9c3d3233ff488b0d)]: - - @clerk/clerk-react@5.25.1 - - @clerk/types@4.49.1 - - @clerk/shared@3.1.0 - -## 0.23.3 - -### Patch Changes - -- Updated dependencies [[`725918d`](https://github.com/clerk/javascript/commit/725918df2e74cea15e9b748aaf103a52df8e8500), [`91d0f0b`](https://github.com/clerk/javascript/commit/91d0f0b0dccab7168ad4dc06c8629808938c235f), [`9572bf5`](https://github.com/clerk/javascript/commit/9572bf5bdfb7dc309ec8714989b98ab12174965b), [`39bbc51`](https://github.com/clerk/javascript/commit/39bbc5189a33dc6cebdc269ac2184dc4ffff2534), [`3dddcda`](https://github.com/clerk/javascript/commit/3dddcda191d8f8d6a9b02464f1f6374d3c6aacb9), [`7524943`](https://github.com/clerk/javascript/commit/7524943300d7e693d61cc1820b520abfadec1c64), [`150b5c8`](https://github.com/clerk/javascript/commit/150b5c89477abb0feab15e0a886179473f653cac), [`23c931e`](https://github.com/clerk/javascript/commit/23c931e9e95e6de992549ad499b477aca9a9c344), [`730262f`](https://github.com/clerk/javascript/commit/730262f0f973923c8749b09078c80c2fc966a8ec), [`0b18bb1`](https://github.com/clerk/javascript/commit/0b18bb1fe6fa3ded97547bb6b4d2c73030aad329), [`021bc5f`](https://github.com/clerk/javascript/commit/021bc5f40044d34e49956ce3c9b61d833d815b42), [`1a61390`](https://github.com/clerk/javascript/commit/1a61390d3482bd4af58508b972ad89dea56fa224)]: - - @clerk/types@4.49.0 - - @clerk/clerk-react@5.25.0 - - @clerk/shared@3.0.2 - -## 0.23.2 - -### Patch Changes - -- Updated dependencies [[`75879672c5805bfba1caca906ac0729497744164`](https://github.com/clerk/javascript/commit/75879672c5805bfba1caca906ac0729497744164), [`7ec95a7e59033600958aca4b86f3bcd5da947dec`](https://github.com/clerk/javascript/commit/7ec95a7e59033600958aca4b86f3bcd5da947dec), [`3c225d90227141dc62d955e76c7f8e0202524bc7`](https://github.com/clerk/javascript/commit/3c225d90227141dc62d955e76c7f8e0202524bc7), [`2a66c16af08573000bb619607346ac420cd4ce56`](https://github.com/clerk/javascript/commit/2a66c16af08573000bb619607346ac420cd4ce56)]: - - @clerk/shared@3.0.1 - - @clerk/clerk-react@5.24.2 - - @clerk/types@4.48.0 - -## 0.23.1 - -### Patch Changes - -- Updated dependencies [[`3d436484eb01b42e0008b6675f4be68f15d21079`](https://github.com/clerk/javascript/commit/3d436484eb01b42e0008b6675f4be68f15d21079)]: - - @clerk/clerk-react@5.24.1 - -## 0.23.0 - -### Minor Changes - -- Surface new `pending` session as a signed-in state ([#5136](https://github.com/clerk/javascript/pull/5136)) by [@LauraBeatris](https://github.com/LauraBeatris) - -### Patch Changes - -- Updated dependencies [[`28179323d9891bd13625e32c5682a3276e73cdae`](https://github.com/clerk/javascript/commit/28179323d9891bd13625e32c5682a3276e73cdae), [`7ae77b74326e378bf161e29886ee82e1556d9840`](https://github.com/clerk/javascript/commit/7ae77b74326e378bf161e29886ee82e1556d9840), [`c5c246ce91c01db9f1eaccbd354f646bcd24ec0a`](https://github.com/clerk/javascript/commit/c5c246ce91c01db9f1eaccbd354f646bcd24ec0a), [`b707e942bfd434ff8a3b9a9fadf9d1b694d702c8`](https://github.com/clerk/javascript/commit/b707e942bfd434ff8a3b9a9fadf9d1b694d702c8), [`bcbe5f6382ebcc70ef4fddb950d052bf6b7d693a`](https://github.com/clerk/javascript/commit/bcbe5f6382ebcc70ef4fddb950d052bf6b7d693a), [`382c30240f563e58bc4d4832557c6825da40ce7f`](https://github.com/clerk/javascript/commit/382c30240f563e58bc4d4832557c6825da40ce7f)]: - - @clerk/types@4.47.0 - - @clerk/shared@3.0.0 - - @clerk/clerk-react@5.24.0 - -## 0.22.23 - -### Patch Changes - -- Updated dependencies [[`d76c4699990b8477745c2584b1b98d5c92f9ace6`](https://github.com/clerk/javascript/commit/d76c4699990b8477745c2584b1b98d5c92f9ace6), [`a9b0087fca3f427f65907b358d9b5bc0c95921d8`](https://github.com/clerk/javascript/commit/a9b0087fca3f427f65907b358d9b5bc0c95921d8), [`92d17d7c087470b262fa5407cb6720fe6b17d333`](https://github.com/clerk/javascript/commit/92d17d7c087470b262fa5407cb6720fe6b17d333), [`30f6f3808e9b3778d5a9eb275780f94f9e9c7651`](https://github.com/clerk/javascript/commit/30f6f3808e9b3778d5a9eb275780f94f9e9c7651)]: - - @clerk/shared@2.22.0 - - @clerk/clerk-react@5.23.0 - - @clerk/types@4.46.1 - -## 0.22.22 - -### Patch Changes - -- Updated dependencies [[`dd2cbfe9f30358b6b298901bb52fa378b0acdca3`](https://github.com/clerk/javascript/commit/dd2cbfe9f30358b6b298901bb52fa378b0acdca3), [`dd2cbfe9f30358b6b298901bb52fa378b0acdca3`](https://github.com/clerk/javascript/commit/dd2cbfe9f30358b6b298901bb52fa378b0acdca3), [`570d8386f6aa596bf7bb1659bdddb8dd4d992b1d`](https://github.com/clerk/javascript/commit/570d8386f6aa596bf7bb1659bdddb8dd4d992b1d), [`570d8386f6aa596bf7bb1659bdddb8dd4d992b1d`](https://github.com/clerk/javascript/commit/570d8386f6aa596bf7bb1659bdddb8dd4d992b1d)]: - - @clerk/clerk-react@5.22.13 - - @clerk/types@4.46.0 - - @clerk/shared@2.21.1 - -## 0.22.21 - -### Patch Changes - -- Updated dependencies [[`f41081c563ddd2afc05b837358e0de087ae0c895`](https://github.com/clerk/javascript/commit/f41081c563ddd2afc05b837358e0de087ae0c895), [`767ac85fe6ce0ee0594c923e9af701bb05f40a0b`](https://github.com/clerk/javascript/commit/767ac85fe6ce0ee0594c923e9af701bb05f40a0b), [`225b38c7187d31fc755155ea99834ca03894d36b`](https://github.com/clerk/javascript/commit/225b38c7187d31fc755155ea99834ca03894d36b), [`429f1bfe5f7a554ab1fdf265475ba6c8b3f78472`](https://github.com/clerk/javascript/commit/429f1bfe5f7a554ab1fdf265475ba6c8b3f78472)]: - - @clerk/shared@2.21.0 - - @clerk/types@4.45.1 - - @clerk/clerk-react@5.22.12 - -## 0.22.20 - -### Patch Changes - -- Updated dependencies [[`0fa449cd09c9973297464a14f785895e3ddcab4d`](https://github.com/clerk/javascript/commit/0fa449cd09c9973297464a14f785895e3ddcab4d)]: - - @clerk/clerk-react@5.22.11 - -## 0.22.19 - -### Patch Changes - -- - `@clerk/clerk-js`, `@clerk/types`: Add `redirectUrl` option to `buildAfterSignInUrl()` and `buildAfterSignUpUrl()` methods. ([#5052](https://github.com/clerk/javascript/pull/5052)) by [@brkalow](https://github.com/brkalow) - - - `@clerk/elements`: Ensure redirect_url params passed to Elements components are always passed to Clerk's underlying `build*Url()` methods. - -- Updated dependencies [[`d3152be7f01fbb5ca26aeddc2437021f4b7ecc83`](https://github.com/clerk/javascript/commit/d3152be7f01fbb5ca26aeddc2437021f4b7ecc83), [`f976349243da2b75023e59e802460e6f3592ebbd`](https://github.com/clerk/javascript/commit/f976349243da2b75023e59e802460e6f3592ebbd)]: - - @clerk/types@4.45.0 - - @clerk/clerk-react@5.22.10 - - @clerk/shared@2.20.18 - -## 0.22.18 - -### Patch Changes - -- Updated dependencies [[`26225f2c31a22560f7ece2e02f1d0080b5b89520`](https://github.com/clerk/javascript/commit/26225f2c31a22560f7ece2e02f1d0080b5b89520), [`833693a6792b621e72162d70673e7bdfa84a69b6`](https://github.com/clerk/javascript/commit/833693a6792b621e72162d70673e7bdfa84a69b6)]: - - @clerk/shared@2.20.17 - - @clerk/clerk-react@5.22.9 - - @clerk/types@4.44.3 - -## 0.22.17 - -### Patch Changes - -- Updated dependencies [[`a309be354275b91a7b17d5a67e8ef6aa230a9935`](https://github.com/clerk/javascript/commit/a309be354275b91a7b17d5a67e8ef6aa230a9935), [`1345cb487970a7347351897e80dfb829d85c41ea`](https://github.com/clerk/javascript/commit/1345cb487970a7347351897e80dfb829d85c41ea)]: - - @clerk/shared@2.20.16 - - @clerk/types@4.44.2 - - @clerk/clerk-react@5.22.8 - -## 0.22.16 - -### Patch Changes - -- Updated dependencies [[`57c983fdc2b8d883623a2294daae0ac6c02c48f6`](https://github.com/clerk/javascript/commit/57c983fdc2b8d883623a2294daae0ac6c02c48f6), [`a26cf0ff10c76244975c454fdf6c615475d4bcd5`](https://github.com/clerk/javascript/commit/a26cf0ff10c76244975c454fdf6c615475d4bcd5)]: - - @clerk/types@4.44.1 - - @clerk/shared@2.20.15 - - @clerk/clerk-react@5.22.7 - -## 0.22.15 - -### Patch Changes - -- Updated dependencies [[`2179690c10a61b117e82fdd566b34939f4d28bc1`](https://github.com/clerk/javascript/commit/2179690c10a61b117e82fdd566b34939f4d28bc1), [`bdb537a9902c0f0ae58ca1d4b7590d929f28fedb`](https://github.com/clerk/javascript/commit/bdb537a9902c0f0ae58ca1d4b7590d929f28fedb)]: - - @clerk/types@4.44.0 - - @clerk/clerk-react@5.22.6 - - @clerk/shared@2.20.14 - -## 0.22.14 - -### Patch Changes - -- Updated dependencies [[`f87ede848265d75ea1e880a3ab80c53a250f42cf`](https://github.com/clerk/javascript/commit/f87ede848265d75ea1e880a3ab80c53a250f42cf), [`6126cc98281bca96797fd8a55b6ec6aeda397e46`](https://github.com/clerk/javascript/commit/6126cc98281bca96797fd8a55b6ec6aeda397e46), [`6e096564a459db4eaf953e99e570905b10be6c84`](https://github.com/clerk/javascript/commit/6e096564a459db4eaf953e99e570905b10be6c84)]: - - @clerk/shared@2.20.13 - - @clerk/types@4.43.0 - - @clerk/clerk-react@5.22.5 - -## 0.22.13 - -### Patch Changes - -- Updated dependencies [[`fe3e49f61acefe8d7f1992405f7cb415fea2e5c8`](https://github.com/clerk/javascript/commit/fe3e49f61acefe8d7f1992405f7cb415fea2e5c8), [`4427c4702f64d4f28f7564ce5889d41e260aa519`](https://github.com/clerk/javascript/commit/4427c4702f64d4f28f7564ce5889d41e260aa519)]: - - @clerk/types@4.42.0 - - @clerk/clerk-react@5.22.4 - - @clerk/shared@2.20.12 - -## 0.22.12 - -### Patch Changes - -- Updated dependencies [[`418ec5c62c4eb600566faab07684c068a29007e3`](https://github.com/clerk/javascript/commit/418ec5c62c4eb600566faab07684c068a29007e3)]: - - @clerk/types@4.41.2 - - @clerk/clerk-react@5.22.3 - - @clerk/shared@2.20.11 - -## 0.22.11 - -### Patch Changes - -- Standardizing ambient declaration files for all SDKs ([#4919](https://github.com/clerk/javascript/pull/4919)) by [@jacekradko](https://github.com/jacekradko) - -- Updated dependencies [[`9eef7713212378351e8e01628611eaa18de250e8`](https://github.com/clerk/javascript/commit/9eef7713212378351e8e01628611eaa18de250e8)]: - - @clerk/shared@2.20.10 - - @clerk/clerk-react@5.22.2 - -## 0.22.10 - -### Patch Changes - -- Support `enterprise_sso` strategy (SAML, OIDC, EASIE) on custom flows with `@clerk/elements` ([#4916](https://github.com/clerk/javascript/pull/4916)) by [@LauraBeatris](https://github.com/LauraBeatris) - -- Updated dependencies [[`7ffc99b48977b9f6c74c0c71c500b60cb8aba65e`](https://github.com/clerk/javascript/commit/7ffc99b48977b9f6c74c0c71c500b60cb8aba65e)]: - - @clerk/types@4.41.1 - - @clerk/clerk-react@5.22.1 - - @clerk/shared@2.20.9 - -## 0.22.9 - -### Patch Changes - -- Updated dependencies [[`4af35380f18d1d06c15ad1f5745c2d5a1ab1c37d`](https://github.com/clerk/javascript/commit/4af35380f18d1d06c15ad1f5745c2d5a1ab1c37d), [`aa48b1f9e890b2402e9d05989a4820141076f7bf`](https://github.com/clerk/javascript/commit/aa48b1f9e890b2402e9d05989a4820141076f7bf), [`53bd34fff38b17498edf66cc4bc2d42d707f28dc`](https://github.com/clerk/javascript/commit/53bd34fff38b17498edf66cc4bc2d42d707f28dc)]: - - @clerk/types@4.41.0 - - @clerk/clerk-react@5.22.0 - - @clerk/shared@2.20.8 - -## 0.22.8 - -### Patch Changes - -- Updated dependencies [[`fd7a5be73db3acaa7daeb9b15af73c2ce99d03a6`](https://github.com/clerk/javascript/commit/fd7a5be73db3acaa7daeb9b15af73c2ce99d03a6)]: - - @clerk/types@4.40.3 - - @clerk/clerk-react@5.21.3 - - @clerk/shared@2.20.7 - -## 0.22.7 - -### Patch Changes - -- Updated dependencies [[`44cab6038af0a4d23869b3b292ece742fbbc4d85`](https://github.com/clerk/javascript/commit/44cab6038af0a4d23869b3b292ece742fbbc4d85)]: - - @clerk/types@4.40.2 - - @clerk/clerk-react@5.21.2 - - @clerk/shared@2.20.6 - -## 0.22.6 - -### Patch Changes - -- Updated dependencies [[`80e1117631d35834705119a79cdcf9e0ed423fdd`](https://github.com/clerk/javascript/commit/80e1117631d35834705119a79cdcf9e0ed423fdd)]: - - @clerk/types@4.40.1 - - @clerk/clerk-react@5.21.1 - - @clerk/shared@2.20.5 - -## 0.22.5 - -### Patch Changes - -- Updated dependencies [[`b5eb15bf81d94456309d6ca44ad423a4175d50b6`](https://github.com/clerk/javascript/commit/b5eb15bf81d94456309d6ca44ad423a4175d50b6), [`b933a2ba8112aefbabd7fe3313b89e083452d2dd`](https://github.com/clerk/javascript/commit/b933a2ba8112aefbabd7fe3313b89e083452d2dd)]: - - @clerk/clerk-react@5.21.0 - -## 0.22.4 - -### Patch Changes - -- Updated dependencies [[`c9da04636ffe1ba804a1ce5e5b79027d3a2344d2`](https://github.com/clerk/javascript/commit/c9da04636ffe1ba804a1ce5e5b79027d3a2344d2)]: - - @clerk/types@4.40.0 - - @clerk/clerk-react@5.20.4 - - @clerk/shared@2.20.4 - -## 0.22.3 - -### Patch Changes - -- Updated dependencies [[`84ccb0049041534f111be65f7c7d4d6120069446`](https://github.com/clerk/javascript/commit/84ccb0049041534f111be65f7c7d4d6120069446)]: - - @clerk/shared@2.20.3 - - @clerk/clerk-react@5.20.3 - -## 0.22.2 - -### Patch Changes - -- Updated dependencies [[`aeafa7c5efd50c893d088ac99199d7eaecc04025`](https://github.com/clerk/javascript/commit/aeafa7c5efd50c893d088ac99199d7eaecc04025), [`acd9326ef2d6942b981b3ee59c4b20ddd303323d`](https://github.com/clerk/javascript/commit/acd9326ef2d6942b981b3ee59c4b20ddd303323d)]: - - @clerk/types@4.39.4 - - @clerk/clerk-react@5.20.2 - - @clerk/shared@2.20.2 - -## 0.22.1 - -### Patch Changes - -- Using the same peerDependencies semver for react and react-dom ([#4758](https://github.com/clerk/javascript/pull/4758)) by [@jacekradko](https://github.com/jacekradko) - -- Handle organization membership quote exceeded error during enterprise SSO ([#4763](https://github.com/clerk/javascript/pull/4763)) by [@mzhong9723](https://github.com/mzhong9723) - -- Updated dependencies [[`66ad299e4b6496ea4a93799de0f1ecfad920ddad`](https://github.com/clerk/javascript/commit/66ad299e4b6496ea4a93799de0f1ecfad920ddad), [`dd3fdc7b2a96ddb90b33c6f1cefb055a60f99a9d`](https://github.com/clerk/javascript/commit/dd3fdc7b2a96ddb90b33c6f1cefb055a60f99a9d), [`e1748582d0c89462f48a482a7805871b7065fa19`](https://github.com/clerk/javascript/commit/e1748582d0c89462f48a482a7805871b7065fa19), [`7f7edcaa8228c26d19e9081979100ada7e982095`](https://github.com/clerk/javascript/commit/7f7edcaa8228c26d19e9081979100ada7e982095), [`dd3fdc7b2a96ddb90b33c6f1cefb055a60f99a9d`](https://github.com/clerk/javascript/commit/dd3fdc7b2a96ddb90b33c6f1cefb055a60f99a9d)]: - - @clerk/shared@2.20.1 - - @clerk/clerk-react@5.20.1 - - @clerk/types@4.39.3 - -## 0.22.0 - -### Minor Changes - -- Handle ticket-based sign in flows such as impersonation ([#4746](https://github.com/clerk/javascript/pull/4746)) by [@LauraBeatris](https://github.com/LauraBeatris) - -- Switching to use ^ for semver ranges of internal @clerk/ production dependencies. ([#4664](https://github.com/clerk/javascript/pull/4664)) by [@jacekradko](https://github.com/jacekradko) - -### Patch Changes - -- Updated dependencies [[`9d656c16bc78ac31b59b5edbd25118dfc33c4469`](https://github.com/clerk/javascript/commit/9d656c16bc78ac31b59b5edbd25118dfc33c4469), [`ffa631d2480cfe77bf08c61b1302ace308e5b630`](https://github.com/clerk/javascript/commit/ffa631d2480cfe77bf08c61b1302ace308e5b630), [`0266f6a73fc34748a86603bc89b6125d6bbb679b`](https://github.com/clerk/javascript/commit/0266f6a73fc34748a86603bc89b6125d6bbb679b)]: - - @clerk/clerk-react@5.20.0 - - @clerk/shared@2.20.0 - -## 0.21.4 - -### Patch Changes - -- Updated dependencies [[`cd72a27a75863dfd94b0a00ed5b2d03231556bc0`](https://github.com/clerk/javascript/commit/cd72a27a75863dfd94b0a00ed5b2d03231556bc0)]: - - @clerk/types@4.39.2 - - @clerk/clerk-react@5.19.3 - - @clerk/shared@2.19.4 - -## 0.21.3 - -### Patch Changes - -- Updated dependencies [[`1b86a1da34ce4bc309f69980ac13a691a0a633c2`](https://github.com/clerk/javascript/commit/1b86a1da34ce4bc309f69980ac13a691a0a633c2)]: - - @clerk/types@4.39.1 - - @clerk/clerk-react@5.19.2 - - @clerk/shared@2.19.3 - -## 0.21.2 - -### Patch Changes - -- Updated dependencies [[`4cb22548da81dd8b186a6ef1cf120aea99c85c62`](https://github.com/clerk/javascript/commit/4cb22548da81dd8b186a6ef1cf120aea99c85c62)]: - - @clerk/shared@2.19.2 - - @clerk/clerk-react@5.19.1 - -## 0.21.1 - -### Patch Changes - -- Updated dependencies [[`3f640805d2a4e1616aafa56f6848d6657911bb99`](https://github.com/clerk/javascript/commit/3f640805d2a4e1616aafa56f6848d6657911bb99), [`550c7e9851329688e37be29b83ea0c3b12482af7`](https://github.com/clerk/javascript/commit/550c7e9851329688e37be29b83ea0c3b12482af7), [`3f640805d2a4e1616aafa56f6848d6657911bb99`](https://github.com/clerk/javascript/commit/3f640805d2a4e1616aafa56f6848d6657911bb99)]: - - @clerk/clerk-react@5.19.0 - - @clerk/types@4.39.0 - - @clerk/shared@2.19.1 - -## 0.21.0 - -### Minor Changes - -- Replace usage of `OAUTH_PROVIDERS` and `WEB3_PROVIDERS` from `@clerk/types` to `@clerk/shared`. ([#4716](https://github.com/clerk/javascript/pull/4716)) by [@panteliselef](https://github.com/panteliselef) - -### Patch Changes - -- Updated dependencies [[`0bc3ccc5bd4a93121bb7e7d6a32271af9c31f8c3`](https://github.com/clerk/javascript/commit/0bc3ccc5bd4a93121bb7e7d6a32271af9c31f8c3), [`0bc3ccc5bd4a93121bb7e7d6a32271af9c31f8c3`](https://github.com/clerk/javascript/commit/0bc3ccc5bd4a93121bb7e7d6a32271af9c31f8c3)]: - - @clerk/shared@2.19.0 - - @clerk/types@4.38.0 - - @clerk/clerk-react@5.18.2 - -## 0.20.1 - -### Patch Changes - -- Updated dependencies [[`4e5e7f463c12893a21cb3b5f9317fc3f2945879b`](https://github.com/clerk/javascript/commit/4e5e7f463c12893a21cb3b5f9317fc3f2945879b)]: - - @clerk/types@4.37.0 - - @clerk/clerk-react@5.18.1 - - @clerk/shared@2.18.1 - -## 0.20.0 - -### Minor Changes - -- Support OKW Wallet Web3 provider and authentication strategy ([#4696](https://github.com/clerk/javascript/pull/4696)) by [@chanioxaris](https://github.com/chanioxaris) - -### Patch Changes - -- Updated dependencies [[`8ee5d84995fa17532491ff96efac5738c9bcd9ef`](https://github.com/clerk/javascript/commit/8ee5d84995fa17532491ff96efac5738c9bcd9ef), [`09fedd1df155d30cc055ce701b133aa6470e9b47`](https://github.com/clerk/javascript/commit/09fedd1df155d30cc055ce701b133aa6470e9b47), [`235eaae4c3c9400492fca47d20a47c7081041565`](https://github.com/clerk/javascript/commit/235eaae4c3c9400492fca47d20a47c7081041565)]: - - @clerk/types@4.36.0 - - @clerk/clerk-react@5.18.0 - - @clerk/shared@2.18.0 - -## 0.19.9 - -### Patch Changes - -- Updated dependencies [[`8a28d1f403309f692d9332704f07effbf39d056d`](https://github.com/clerk/javascript/commit/8a28d1f403309f692d9332704f07effbf39d056d)]: - - @clerk/types@4.35.1 - - @clerk/clerk-react@5.17.2 - - @clerk/shared@2.17.1 - -## 0.19.8 - -### Patch Changes - -- Updated dependencies [[`115fd0c32443c6fc4e692c0ebdd60c092e57057e`](https://github.com/clerk/javascript/commit/115fd0c32443c6fc4e692c0ebdd60c092e57057e), [`0a1807552dcf0501a97f60b4df0280525bca9743`](https://github.com/clerk/javascript/commit/0a1807552dcf0501a97f60b4df0280525bca9743)]: - - @clerk/clerk-react@5.17.1 - -## 0.19.7 - -### Patch Changes - -- Updated dependencies [[`4da28fa857d1e5538eb5bbe28ecc4bf9dba1ce7d`](https://github.com/clerk/javascript/commit/4da28fa857d1e5538eb5bbe28ecc4bf9dba1ce7d), [`fe9e147e366153d664af7fc325655ecb299a1f9d`](https://github.com/clerk/javascript/commit/fe9e147e366153d664af7fc325655ecb299a1f9d), [`fe9e147e366153d664af7fc325655ecb299a1f9d`](https://github.com/clerk/javascript/commit/fe9e147e366153d664af7fc325655ecb299a1f9d), [`fe9e147e366153d664af7fc325655ecb299a1f9d`](https://github.com/clerk/javascript/commit/fe9e147e366153d664af7fc325655ecb299a1f9d), [`d84d7e31235c5c7da3415981dc76db4473a71a39`](https://github.com/clerk/javascript/commit/d84d7e31235c5c7da3415981dc76db4473a71a39), [`dce4f7ffca7248c0500f0ec9a978672b1f2fad69`](https://github.com/clerk/javascript/commit/dce4f7ffca7248c0500f0ec9a978672b1f2fad69)]: - - @clerk/clerk-react@5.17.0 - - @clerk/shared@2.17.0 - - @clerk/types@4.35.0 - -## 0.19.6 - -### Patch Changes - -- Updated dependencies [[`c70994b5b6f92a6550dfe37547f01bbfa810c223`](https://github.com/clerk/javascript/commit/c70994b5b6f92a6550dfe37547f01bbfa810c223), [`7623a99594e7329200b6b374e483152d7679ce66`](https://github.com/clerk/javascript/commit/7623a99594e7329200b6b374e483152d7679ce66)]: - - @clerk/types@4.34.2 - - @clerk/clerk-react@5.16.2 - - @clerk/shared@2.16.1 - -## 0.19.5 - -### Patch Changes - -- Updated dependencies [[`e47eb5882a7fd4a8dee25933c6644790d6ea3407`](https://github.com/clerk/javascript/commit/e47eb5882a7fd4a8dee25933c6644790d6ea3407), [`273d16cb0665d4d960838cb294dc356f41814745`](https://github.com/clerk/javascript/commit/273d16cb0665d4d960838cb294dc356f41814745), [`6b0961765e1f3d09679be4b163fa13ac7dd97191`](https://github.com/clerk/javascript/commit/6b0961765e1f3d09679be4b163fa13ac7dd97191)]: - - @clerk/clerk-react@5.16.1 - - @clerk/shared@2.16.0 - - @clerk/types@4.34.1 - -## 0.19.4 - -### Patch Changes - -- Updated dependencies [[`536fa996ff84a545678a3036b28409824d1c00dd`](https://github.com/clerk/javascript/commit/536fa996ff84a545678a3036b28409824d1c00dd), [`b2671affd230eed176ac03af516307898d371757`](https://github.com/clerk/javascript/commit/b2671affd230eed176ac03af516307898d371757), [`b28c5e8bc44885bf6b1533df48e872ba90c387da`](https://github.com/clerk/javascript/commit/b28c5e8bc44885bf6b1533df48e872ba90c387da), [`6c424e179850f520ae738e816bf0423a55ddf3ef`](https://github.com/clerk/javascript/commit/6c424e179850f520ae738e816bf0423a55ddf3ef)]: - - @clerk/shared@2.15.0 - - @clerk/clerk-react@5.16.0 - -## 0.19.3 - -### Patch Changes - -- Updated dependencies [[`46faeb6f59b19c963fb137c858347525b1cd9e19`](https://github.com/clerk/javascript/commit/46faeb6f59b19c963fb137c858347525b1cd9e19)]: - - @clerk/types@4.34.0 - -## 0.19.2 - -### Patch Changes - -- Display additional errors from enterprise SSO ([#4553](https://github.com/clerk/javascript/pull/4553)) by [@LauraBeatris](https://github.com/LauraBeatris) - -- Export `` from root sign-in exports. ([#4548](https://github.com/clerk/javascript/pull/4548)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Updated dependencies [[`1c0b5001f7f975a2f3f54ad692526ecf7257847e`](https://github.com/clerk/javascript/commit/1c0b5001f7f975a2f3f54ad692526ecf7257847e), [`6217a3f7c94311d49f873214bd406961e0b8d6f7`](https://github.com/clerk/javascript/commit/6217a3f7c94311d49f873214bd406961e0b8d6f7), [`1783025cdb94c447028704c2121fa0b8af785904`](https://github.com/clerk/javascript/commit/1783025cdb94c447028704c2121fa0b8af785904)]: - - @clerk/types@4.33.0 - -## 0.19.1 - -### Patch Changes - -- Updated dependencies [[`7dbad4c5abd226d7b10941a626ead5d85b1a3f24`](https://github.com/clerk/javascript/commit/7dbad4c5abd226d7b10941a626ead5d85b1a3f24)]: - - @clerk/types@4.32.0 - -## 0.19.0 - -### Minor Changes - -- Introduce support for `` and ``. This allows rendering of a CAPTCHA widget when a sign in attempt is transferred to a sign up attempt. ([#4523](https://github.com/clerk/javascript/pull/4523)) by [@BRKalow](https://github.com/BRKalow) - -- The Legal consent feature is now stable. ([#4487](https://github.com/clerk/javascript/pull/4487)) by [@octoper](https://github.com/octoper) - - Removed the `__experimental_` prefix. - -### Patch Changes - -- Fixes issues in `ClerkRouter` that were causing inaccurate pathnames within Elements flows. Also fixes a dependency issue where `@clerk/elements` was pulling in the wrong version of `@clerk/shared`. ([#4513](https://github.com/clerk/javascript/pull/4513)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Updated dependencies [[`f7472e22877f62fc7f3c8d3efe409ff2276fb4a3`](https://github.com/clerk/javascript/commit/f7472e22877f62fc7f3c8d3efe409ff2276fb4a3), [`e199037b8f484abdeeb9fc24455a1b4b8c31c8dd`](https://github.com/clerk/javascript/commit/e199037b8f484abdeeb9fc24455a1b4b8c31c8dd), [`0e443ad7c76643420b50e5b169193e03f6ef79f9`](https://github.com/clerk/javascript/commit/0e443ad7c76643420b50e5b169193e03f6ef79f9), [`cc24c8145f1eea7fb91550f2c3e0bac3993e4320`](https://github.com/clerk/javascript/commit/cc24c8145f1eea7fb91550f2c3e0bac3993e4320)]: - - @clerk/types@4.31.0 - -## 0.18.5 - -### Patch Changes - -- Updated dependencies [[`8a04ae47b8305f994b348301fd8134d5baf02943`](https://github.com/clerk/javascript/commit/8a04ae47b8305f994b348301fd8134d5baf02943)]: - - @clerk/shared@2.11.5 - -## 0.18.4 - -### Patch Changes - -- Add Elements `` component. ([#4456](https://github.com/clerk/javascript/pull/4456)) by [@alexcarpenter](https://github.com/alexcarpenter) - - ```tsx - import * as Clerk from '@clerk/elements/common'; - import NextLink from 'next/link'; - - function SignInPage() { - return ( - <> - Sign up - - {url => Sign up} - - ); - } - ``` - -- Updated dependencies [[`d74a6a7c0f291104c6bba722a8c432814d7b336e`](https://github.com/clerk/javascript/commit/d74a6a7c0f291104c6bba722a8c432814d7b336e), [`1a0c8fe665869e732d3c800bde0f5219fce54301`](https://github.com/clerk/javascript/commit/1a0c8fe665869e732d3c800bde0f5219fce54301), [`0800fc3f1f4e1b6a1d13f5c02557001a283af6e8`](https://github.com/clerk/javascript/commit/0800fc3f1f4e1b6a1d13f5c02557001a283af6e8)]: - - @clerk/types@4.30.0 - - @clerk/shared@2.11.4 - -## 0.18.3 - -### Patch Changes - -- Use host router instead of directly calling Next's `useRouter` hook by [@nikosdouvlis](https://github.com/nikosdouvlis) - -- Updated dependencies [[`a7726cc12a824b278f6d2a37cb1901c38c5f70dc`](https://github.com/clerk/javascript/commit/a7726cc12a824b278f6d2a37cb1901c38c5f70dc)]: - - @clerk/shared@2.11.3 - -## 0.18.0 - -### Minor Changes - -- Added support for `__experimental_legalAccepted` field ([#4427](https://github.com/clerk/javascript/pull/4427)) by [@octoper](https://github.com/octoper) - -### Patch Changes - -- - Introduce `redirectUrl` property on `setActive` as a replacement for `beforeEmit`. ([#4312](https://github.com/clerk/javascript/pull/4312)) by [@issuedat](https://github.com/issuedat) - - - Deprecates `beforeEmit` property on `setActive`. - -- Updated dependencies [[`f875463da`](https://github.com/clerk/javascript/commit/f875463da9692f2d173b6d5388743cf720750ae3), [`5be7ca9fd`](https://github.com/clerk/javascript/commit/5be7ca9fd239c937cc88e20ce8f5bfc9f3b84f22), [`08c5a2add`](https://github.com/clerk/javascript/commit/08c5a2add6872c76e62fc0df06db723e3728452e), [`434b432f8`](https://github.com/clerk/javascript/commit/434b432f8c114825120eef0f2c278b8142ed1563)]: - - @clerk/types@4.29.0 - - @clerk/shared@2.11.0 - -## 0.17.1 - -### Patch Changes - -- Updated dependencies [[`3fdcdbf88`](https://github.com/clerk/javascript/commit/3fdcdbf88c38facf8b82563f634ec1b6604fd8e5)]: - - @clerk/types@4.28.0 - - @clerk/shared@2.10.1 - -## 0.17.0 - -### Minor Changes - -- Add experimental support for new UI components ([#4114](https://github.com/clerk/javascript/pull/4114)) by [@BRKalow](https://github.com/BRKalow) - -### Patch Changes - -- Remove @clerk/clerk-react as a dev depedency. Move @clerk/shared to dependencies (previously devDepedencies). ([#4114](https://github.com/clerk/javascript/pull/4114)) by [@BRKalow](https://github.com/BRKalow) - -- Updated dependencies [[`3b50b67bd`](https://github.com/clerk/javascript/commit/3b50b67bd40da33c9e36773aa05462717e9f44cc), [`3b50b67bd`](https://github.com/clerk/javascript/commit/3b50b67bd40da33c9e36773aa05462717e9f44cc)]: - - @clerk/shared@2.10.0 - - @clerk/types@4.27.0 - -## 0.16.2 - -### Patch Changes - -- Updated dependencies [[`e81d45b72`](https://github.com/clerk/javascript/commit/e81d45b72c81403c7c206dac5454de1fef6bec57), [`99cdf9b67`](https://github.com/clerk/javascript/commit/99cdf9b67d1e99e66cc73d8a5bfce1f1f8df1b83), [`ce40ff6f0`](https://github.com/clerk/javascript/commit/ce40ff6f0d3bc79e33375be6dd5e03f140a07000), [`2102052c0`](https://github.com/clerk/javascript/commit/2102052c017065ab511339870fcebaa6719f2702)]: - - @clerk/types@4.26.0 - -## 0.16.1 - -### Patch Changes - -- Fixes a bug during a ticket-based sign-up where the form could not be submitted if additional fields were needed. ([#4318](https://github.com/clerk/javascript/pull/4318)) by [@BRKalow](https://github.com/BRKalow) - -- Updated dependencies [[`2ba2fd148`](https://github.com/clerk/javascript/commit/2ba2fd1483b7561d7df9a1952ead0ee15e422131)]: - - @clerk/types@4.25.1 - -## 0.16.0 - -### Minor Changes - -- Adds `restricted` Step for restricted sign-up mode ([#4221](https://github.com/clerk/javascript/pull/4221)) by [@tmilewski](https://github.com/tmilewski) - -## 0.15.10 - -### Patch Changes - -- Remove "example mode" guard form "AUTHENICTATE.PASSKEY" event in verification flow ([#4295](https://github.com/clerk/javascript/pull/4295)) by [@tmilewski](https://github.com/tmilewski) - -## 0.15.9 - -### Patch Changes - -- Updated dependencies [[`fb932e5cf`](https://github.com/clerk/javascript/commit/fb932e5cf21315adf60bee0855b6bd5ee2ff9867)]: - - @clerk/types@4.25.0 - -## 0.15.8 - -### Patch Changes - -- Updated dependencies [[`f6fb8b53d`](https://github.com/clerk/javascript/commit/f6fb8b53d236863ad7eca576ee7a16cd33f3506b), [`4a8570590`](https://github.com/clerk/javascript/commit/4a857059059a02bb4f20893e08601e1e67babbed)]: - - @clerk/types@4.24.0 - -## 0.15.7 - -### Patch Changes - -- Updated dependencies [[`4749ed4c5`](https://github.com/clerk/javascript/commit/4749ed4c55a5ba5810451b8d436aad0d49829050), [`f1f17eaab`](https://github.com/clerk/javascript/commit/f1f17eaabed0dc4b7de405fb77d85503cf75ad33), [`2e35ac538`](https://github.com/clerk/javascript/commit/2e35ac53885f8008779940d41d1e804fa77ebfa9)]: - - @clerk/types@4.23.0 - -## 0.15.6 - -### Patch Changes - -- Updated dependencies [[`c9063853e`](https://github.com/clerk/javascript/commit/c9063853e538a4010f5d4e522a3da5abc80098a4), [`19d3808d4`](https://github.com/clerk/javascript/commit/19d3808d4672234944226d6709ec51214e8d6e1d), [`737bcbb0f`](https://github.com/clerk/javascript/commit/737bcbb0ffb5e2dcadbb02e8fc718fe8825c5842)]: - - @clerk/types@4.22.0 - -## 0.15.5 - -### Patch Changes - -- Internal change to move `iconImageUrl` util to `shared` package. ([#4188](https://github.com/clerk/javascript/pull/4188)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Updated dependencies [[`2e5c550e4`](https://github.com/clerk/javascript/commit/2e5c550e4aec61150c2a17fdcd4a0e1273cb50e7)]: - - @clerk/types@4.21.1 - -## 0.15.4 - -## 0.15.3 - -### Patch Changes - -- Updated dependencies [[`248142a6d`](https://github.com/clerk/javascript/commit/248142a6ded6ca937d0df7d628197f25228aadec), [`1189f71f8`](https://github.com/clerk/javascript/commit/1189f71f872f2683c12de5add5f154aeb953ca8d)]: - - @clerk/types@4.21.0 - -## 0.15.2 - -## 0.15.1 - -### Patch Changes - -- Updated dependencies [[`8c6909d46`](https://github.com/clerk/javascript/commit/8c6909d46328c943f1d464a28f1a324a27d0f3f1)]: - - @clerk/types@4.20.1 - -## 0.15.0 - -### Minor Changes - -- Remove `@clerk/elements` reliance on `next` and `@clerk/clerk-react` directly. The host router is now provided by `@clerk/nextjs`. ([#4064](https://github.com/clerk/javascript/pull/4064)) by [@BRKalow](https://github.com/BRKalow) - -### Patch Changes - -- Add support for Coinbase Wallet Web3 provider ([#4103](https://github.com/clerk/javascript/pull/4103)) by [@chanioxaris](https://github.com/chanioxaris) - -- Adds support for `asChild` prop within `choose-strategy` and `choose-session` sign-in steps. ([#4094](https://github.com/clerk/javascript/pull/4094)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Drop support for deprecated Coinbase Web3 provider ([#4092](https://github.com/clerk/javascript/pull/4092)) by [@chanioxaris](https://github.com/chanioxaris) - -- Fixes issue where errors were incorrectly being returned as an `any` type. ([#4119](https://github.com/clerk/javascript/pull/4119)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Allow for passkey triggers in the verification steps ([#4093](https://github.com/clerk/javascript/pull/4093)) by [@tmilewski](https://github.com/tmilewski) - -- Updated dependencies [[`c63a5adf0`](https://github.com/clerk/javascript/commit/c63a5adf0ba4b99252146f168318f51b709bb5dd), [`8823c21a2`](https://github.com/clerk/javascript/commit/8823c21a26bc81cbc3ed007908b1a9ea474bd343), [`a0cb062fa`](https://github.com/clerk/javascript/commit/a0cb062faa4d23bef7a577e5cc486f4c5efe6bfa)]: - - @clerk/types@4.20.0 - -## 0.14.6 - -### Patch Changes - -- Updated dependencies [[`8a3b9f079`](https://github.com/clerk/javascript/commit/8a3b9f0793484b32dd609a5c80a194e62151d6ea), [`e95c28196`](https://github.com/clerk/javascript/commit/e95c2819675cea7963f2404e5f71f37ebed8d5e0)]: - - @clerk/types@4.19.0 - -## 0.14.5 - -### Patch Changes - -- Add support for sign in and sign up with Coinbase ([#4051](https://github.com/clerk/javascript/pull/4051)) by [@EmmanouelaPothitou](https://github.com/EmmanouelaPothitou) - -- Updated dependencies [[`82593173a`](https://github.com/clerk/javascript/commit/82593173aafbf6646e12c5779627cdcb138a1f27), [`afad9af89`](https://github.com/clerk/javascript/commit/afad9af893984a19d7284f0ad3b36e7891d0d733)]: - - @clerk/types@4.18.0 - -## 0.14.4 - -### Patch Changes - -- Tidy up and improve README ([#4053](https://github.com/clerk/javascript/pull/4053)) by [@LekoArts](https://github.com/LekoArts) - -- Moves the common `ClerkRouter` interface into `@clerk/shared/router`. Elements has been refactored internally to import the router from the shared package. ([#4045](https://github.com/clerk/javascript/pull/4045)) by [@BRKalow](https://github.com/BRKalow) - -- Updated dependencies [[`58e6754ad`](https://github.com/clerk/javascript/commit/58e6754ad9f9a1244b023ce1f5e5f2c1c4eb20e7), [`13693018f`](https://github.com/clerk/javascript/commit/13693018f4f7ac5d224698aa730e20960896f68c), [`3304dcc0b`](https://github.com/clerk/javascript/commit/3304dcc0bc93a92a7f729f585c60ff91d2ae04f6)]: - - @clerk/types@4.17.0 - -## 0.14.3 - -### Patch Changes - -- Updated dependencies [[`c1389492d`](https://github.com/clerk/javascript/commit/c1389492d8b6a9292ab04889bf776c0f45e66845)]: - - @clerk/types@4.16.0 - -## 0.14.2 - -### Patch Changes - -- Updated dependencies [[`0158c774a`](https://github.com/clerk/javascript/commit/0158c774af2243a2cd13b55c4d6fae877178c961), [`8be1a7abc`](https://github.com/clerk/javascript/commit/8be1a7abc8849d7d59552011bd6b25bc917d51f5)]: - - @clerk/types@4.15.1 - -## 0.14.1 - -### Patch Changes - -- Fix `SignIn.SafeIdentifier` potentially outputting an incorrect identifier when using similar multi-session sign in strategies. ([#3974](https://github.com/clerk/javascript/pull/3974)) by [@tmilewski](https://github.com/tmilewski) - -- Updated dependencies [[`247b3fd75`](https://github.com/clerk/javascript/commit/247b3fd75042365dc9f950db056b76f9fadfdcf6)]: - - @clerk/types@4.15.0 - -## 0.14.0 - -### Minor Changes - -- Introduce multi-session choose account step and associated actions/components. ([#3957](https://github.com/clerk/javascript/pull/3957)) by [@tmilewski](https://github.com/tmilewski) - - Example: - - ```tsx - - - - {({ session }) => ( - <> - {session.identifier} | Switch... - - )} - - - - ``` - -### Patch Changes - -- Refactor form hooks and utils into separate files ([#3933](https://github.com/clerk/javascript/pull/3933)) by [@tmilewski](https://github.com/tmilewski) - -- feat(elements): Add support for sign in with email_link via redirect_url ([#3926](https://github.com/clerk/javascript/pull/3926)) by [@dstaley](https://github.com/dstaley) - -- Preserve absolute URLs passed via `redirect_url` ([#3947](https://github.com/clerk/javascript/pull/3947)) by [@dstaley](https://github.com/dstaley) - -- Extract common Form components from single file ([#3933](https://github.com/clerk/javascript/pull/3933)) by [@tmilewski](https://github.com/tmilewski) - -- Add support for submit and passkey loading scopes ([#3927](https://github.com/clerk/javascript/pull/3927)) by [@dstaley](https://github.com/dstaley) - -- Updated dependencies [[`dc0e1c33d`](https://github.com/clerk/javascript/commit/dc0e1c33d6844b028cb1ee11c3359b886d609f3c)]: - - @clerk/types@4.14.0 - -## 0.13.0 - -### Minor Changes - -- Handle ticket-based invitation sign-up workflows ([#3910](https://github.com/clerk/javascript/pull/3910)) by [@tmilewski](https://github.com/tmilewski) - -### Patch Changes - -- Refactor form hooks and utils into separate files ([#3931](https://github.com/clerk/javascript/pull/3931)) by [@tmilewski](https://github.com/tmilewski) - -- In certain situations the Frontend API response contains [`supported_first_factors`](https://clerk.com/docs/reference/frontend-api/tag/Sign-Ins#operation/createSignIn!c=200&path=response/supported_first_factors&t=response) with a `null` value while the current code always assumed to receive an array. `SignInResource['supportedFirstFactors']` has been updated to account for that and any code accessing this value has been made more resilient against `null` values. ([#3938](https://github.com/clerk/javascript/pull/3938)) by [@dstaley](https://github.com/dstaley) - -- Updated dependencies [[`b6f0613dc`](https://github.com/clerk/javascript/commit/b6f0613dc9d8b0bab41cfabbaa8621b126e3bdf5)]: - - @clerk/types@4.13.1 - -## 0.12.4 - -### Patch Changes - -- Add support for redirect_url URL parameter ([#3906](https://github.com/clerk/javascript/pull/3906)) by [@dstaley](https://github.com/dstaley) - -- Updated dependencies [[`4e6c94e3f`](https://github.com/clerk/javascript/commit/4e6c94e3f4cc92cbba8bddcd2b90fcc9cfb83763)]: - - @clerk/types@4.13.0 - -## 0.12.3 - -### Patch Changes - -- Updated dependencies [[`9b2aeacb3`](https://github.com/clerk/javascript/commit/9b2aeacb32fff7c300bda458636a1cc81a42ee7b)]: - - @clerk/types@4.12.1 - -## 0.12.2 - -### Patch Changes - -- Return password validation errors with additional supporting information from instance configuration ([#3812](https://github.com/clerk/javascript/pull/3812)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Fixes a bug that briefly showed the underlying primitive input for OTPs when auto-filled in MacOS ([#3899](https://github.com/clerk/javascript/pull/3899)) by [@joe-bell](https://github.com/joe-bell) - -- Updated dependencies [[`7e94fcf0f`](https://github.com/clerk/javascript/commit/7e94fcf0fcbee8842a54f7931c45190370aa870d)]: - - @clerk/types@4.12.0 - -## 0.12.1 - -### Patch Changes - -- Updated dependencies [[`568186cad`](https://github.com/clerk/javascript/commit/568186cad29acaf0b084a9f86ccb9d29bd23fcf4), [`407195270`](https://github.com/clerk/javascript/commit/407195270ed8aab6eef18c64a4918e3870fef471)]: - - @clerk/types@4.11.0 - -## 0.12.0 - -### Minor Changes - -- Add Metamask (Web3) support for sign in and sign up ([#3879](https://github.com/clerk/javascript/pull/3879)) by [@dstaley](https://github.com/dstaley) - -## 0.11.0 - -### Minor Changes - -- Add full SAML support ([#3842](https://github.com/clerk/javascript/pull/3842)) by [@tmilewski](https://github.com/tmilewski) - -- Update signin `isLoggedInAndSingleSession` guard to navigate using `buildAfterSignInUrl` when true. ([#3841](https://github.com/clerk/javascript/pull/3841)) by [@alexcarpenter](https://github.com/alexcarpenter) - -### Patch Changes - -- Fixes issue where the incorrect sign in first factor strategy was being returned during sign in. ([#3828](https://github.com/clerk/javascript/pull/3828)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Ensure correct supported strategies are rendered based on first or second factor needs. ([#3843](https://github.com/clerk/javascript/pull/3843)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Updated dependencies [[`aa06f3ba7`](https://github.com/clerk/javascript/commit/aa06f3ba7e725071c90d4a1d6840060236da3c23), [`80e647731`](https://github.com/clerk/javascript/commit/80e64773135865434cf0e6c220e287397aa07937)]: - - @clerk/types@4.10.0 - -## 0.10.7 - -### Patch Changes - -- Updated dependencies [[`b48689705`](https://github.com/clerk/javascript/commit/b48689705f9fc2251d2f24addec7a0d0b1da0fe1)]: - - @clerk/types@4.9.1 - -## 0.10.6 - -### Patch Changes - -- Add support for checkbox input usage and `signOutOfOtherSessions` functionality ([#3791](https://github.com/clerk/javascript/pull/3791)) by [@alexcarpenter](https://github.com/alexcarpenter) - -## 0.10.5 - -### Patch Changes - -- Reverts [addition of relatedTarget check](https://github.com/clerk/javascript/pull/3762) in onFocus event handler which prevented fieldstate info from render on focus. ([#3770](https://github.com/clerk/javascript/pull/3770)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Handle call to `hasTags` on undefined `state` ([#3738](https://github.com/clerk/javascript/pull/3738)) by [@tmilewski](https://github.com/tmilewski) - -- Fixes issue where an invalid password field was immediately being refocused after submission causing the validation to run and show the success state. ([#3762](https://github.com/clerk/javascript/pull/3762)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Pass resource directly to machine over getSnapshot to avoid empty context ([#3738](https://github.com/clerk/javascript/pull/3738)) by [@tmilewski](https://github.com/tmilewski) - -- Fix issue where password field validation was incorrectly showing successful field state due to input being refocused on invalid form submission ([#3778](https://github.com/clerk/javascript/pull/3778)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Update XState from 5.13.x to 5.15.x ([#3738](https://github.com/clerk/javascript/pull/3738)) by [@tmilewski](https://github.com/tmilewski) - -- Update types to account for null second factors ([#3780](https://github.com/clerk/javascript/pull/3780)) by [@dstaley](https://github.com/dstaley) - -- Add support for `transform` prop on `SignIn.SafeIdentifier` and determine identifier based on strategy ([#3749](https://github.com/clerk/javascript/pull/3749)) by [@dstaley](https://github.com/dstaley) - -- Updated dependencies [[`b2788f67b`](https://github.com/clerk/javascript/commit/b2788f67b75cce17af1a2f91a984bb826a5a42e1), [`86c75e50c`](https://github.com/clerk/javascript/commit/86c75e50cba9c4efb480672f1b8c6a6fff4ef477)]: - - @clerk/types@4.9.0 - -## 0.10.4 - -### Patch Changes - -- Fix issue where default field values were being set and clearing field errors. ([#3736](https://github.com/clerk/javascript/pull/3736)) by [@alexcarpenter](https://github.com/alexcarpenter) - - Fix issue where resendable UI in the email_link verification step was not updating on click. - -## 0.10.3 - -### Patch Changes - -- Ensure updated provided values to controlled inputs are sent to the machine by [@nikosdouvlis](https://github.com/nikosdouvlis) - -- Fix isWeb3Strategy check to account for prefix and suffix by [@nikosdouvlis](https://github.com/nikosdouvlis) - -- Updated dependencies [[`df7d856d5`](https://github.com/clerk/javascript/commit/df7d856d56bc3b1dcbdbf9155b4ef1b1ea5971f7)]: - - @clerk/types@4.8.0 - -## 0.10.2 - -### Patch Changes - -- Prefill populated fields when navigating back to the start step from the verify step ([#3685](https://github.com/clerk/javascript/pull/3685)) by [@alexcarpenter](https://github.com/alexcarpenter) - -## 0.10.1 - -## 0.10.0 - -### Minor Changes - -- Add `backup_code` verification strategy ([#3627](https://github.com/clerk/javascript/pull/3627)) by [@tmilewski](https://github.com/tmilewski) - - ```tsx - - Use a backup code - - ``` - - ```tsx - - - - Code: - - - - - Continue - - - ``` - -### Patch Changes - -- Addresses the issue where sign-in factors were not properly falling back to empty arrays. ([#3647](https://github.com/clerk/javascript/pull/3647)) by [@LauraBeatris](https://github.com/LauraBeatris) - -- Refactors sign-up loading logic to be in-line with sign-in ([#3648](https://github.com/clerk/javascript/pull/3648)) by [@tmilewski](https://github.com/tmilewski) - -- Ensure Sign Up resending resets upon being triggered ([#3652](https://github.com/clerk/javascript/pull/3652)) by [@tmilewski](https://github.com/tmilewski) - -- Fixes persistent loading states within the `forgot-password` step ([#3648](https://github.com/clerk/javascript/pull/3648)) by [@tmilewski](https://github.com/tmilewski) - -- Fix Sign In forgot-password step not rendering ([#3653](https://github.com/clerk/javascript/pull/3653)) by [@tmilewski](https://github.com/tmilewski) - -## 0.9.2 - -### Patch Changes - -- Updated dependencies [[`d6b5006c4`](https://github.com/clerk/javascript/commit/d6b5006c4cc1b6f07bb3a6832b4ec6e65ea15814)]: - - @clerk/types@4.7.0 - -## 0.9.1 - -### Patch Changes - -- Add a development-only warning for cases when a user renders a `` component that isn't activated for their Clerk instance. As this can be intended behavior (e.g. build out a full example and let user enable/disable stuff solely in the dashboard) the warning can safely be ignored if necessary. ([#3609](https://github.com/clerk/javascript/pull/3609)) by [@LekoArts](https://github.com/LekoArts) - -## 0.9.0 - -### Minor Changes - -- Improve `` and re-organize some data attributes related to validity states. These changes might be breaking changes for you. ([#3594](https://github.com/clerk/javascript/pull/3594)) by [@LekoArts](https://github.com/LekoArts) - - Overview of changes: - - `
` no longer has `data-valid` and `data-invalid` attributes. If there are global errors (same heuristics as ``) then a `data-global-error` attribute will be present. - - Fixed a bug where `` could contain `data-valid` and `data-invalid` at the same time. - - The field state (accessible through e.g. ``) now also incorporates the field's [ValidityState](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) into its output. If the `ValidityState` is invalid, the field state will be an `error`. You can access this information in three places: - 1. `` - 2. `data-state` attribute on `` - 3. `{(state) =>

Field's state is {state}

}
` - -### Patch Changes - -- Fix Sign In & Sign Up root fallbacks not rendering as expected ([#3601](https://github.com/clerk/javascript/pull/3601)) by [@tmilewski](https://github.com/tmilewski) - -- Update all Radix dependencies to their June 19, 2024 release ([#3606](https://github.com/clerk/javascript/pull/3606)) by [@LekoArts](https://github.com/LekoArts) - -## 0.8.0 - -### Minor Changes - -- The `path` prop on the `` and `` component is now automatically inferred. Previously, the default values were `/sign-in` and `/sign-up`, on other routes you had to explicitly define your route. ([#3557](https://github.com/clerk/javascript/pull/3557)) by [@LekoArts](https://github.com/LekoArts) - - The new heuristic for determining the path where `` and `` are mounted is: - 1. `path` prop - 2. Automatically inferred - 3. If it can't be inferred, fallback to `CLERK_SIGN_IN_URL` and `CLERK_SIGN_UP_URL` env var - 4. Fallback to `/sign-in` and `/sign-up` - -### Patch Changes - -- Render the resendable button at the 0 tick ([#3575](https://github.com/clerk/javascript/pull/3575)) by [@alexcarpenter](https://github.com/alexcarpenter) - -- Updated dependencies [[`1273b04ec`](https://github.com/clerk/javascript/commit/1273b04ecf1866b59ef59a74abe31dbcc726da2c)]: - - @clerk/types@4.6.1 - -## 0.7.0 - -### Minor Changes - -- Support passkeys in `` flows. ([#3472](https://github.com/clerk/javascript/pull/3472)) by [@panteliselef](https://github.com/panteliselef) - - APIs introduced: - - `` - - `` - - `` - - Detects the usage of `webauthn` to trigger passkey autofill `` - - Usage examples: - - `` - - ```tsx - - - {isLoading => (isLoading ? : 'Use passkey instead')}. - - - ``` - - - `` - - ```tsx - - - - ``` - - - `` - - ```tsx - -

- Welcome back ! -

- - Continue with Passkey -
- ``` - - - Passkey Autofill - ```tsx - - - Email - - - - - ``` - -## 0.6.0 - -### Minor Changes - -- - Adds virtual router to support modal scenarios ([#3461](https://github.com/clerk/javascript/pull/3461)) by [@tmilewski](https://github.com/tmilewski) - - - Adds `routing` prop to `SignIn.Root` and `SignUp.Root` for handling `virtual` routing - - Better support for Account Portal redirect callback flows - -### Patch Changes - -- Fix forms unable to submit upon re-mounting ([#3473](https://github.com/clerk/javascript/pull/3473)) by [@tmilewski](https://github.com/tmilewski) - -- Set `@clerk/types` as a dependency for packages that had it as a dev dependency. ([#3450](https://github.com/clerk/javascript/pull/3450)) by [@desiprisg](https://github.com/desiprisg) - -- Ensure missing passwordSettings don't throw an error ([#3474](https://github.com/clerk/javascript/pull/3474)) by [@tmilewski](https://github.com/tmilewski) - -- Display hard to catch errors inside the sign-in verification step during development (when `NODE_ENV` is set to `development`). ([#3517](https://github.com/clerk/javascript/pull/3517)) by [@LekoArts](https://github.com/LekoArts) - -- Updated dependencies [[`73e5d61e2`](https://github.com/clerk/javascript/commit/73e5d61e21ab3f77f3c8343bc63da0626466c7ac), [`b8e46328d`](https://github.com/clerk/javascript/commit/b8e46328da874859c4928f19f924219cd6520b11)]: - - @clerk/types@4.6.0 - -## 0.5.2 - -### Patch Changes - -- Widen optional peerDependency of `next` to include `>=15.0.0-rc`. This way you can use Next.js 15 with Clerk Elements without your package manager complaining. Also allow React 19. ([#3445](https://github.com/clerk/javascript/pull/3445)) by [@LekoArts](https://github.com/LekoArts) - -## 0.5.1 - -### Patch Changes - -- Update the TypeScript type of `` to allow the `validatePassword` prop also on `type="text"` (in addition to `type="password"`) ([#3394](https://github.com/clerk/javascript/pull/3394)) by [@LekoArts](https://github.com/LekoArts) - -## 0.5.0 - -### Minor Changes - -- - Adds Stately's Browser Inspector in development builds ([#3424](https://github.com/clerk/javascript/pull/3424)) by [@tmilewski](https://github.com/tmilewski) - - - Removes `@statelyai/inspect` from dependencies - - Ensures all inspector-related code is omitted from the build - -### Patch Changes - -- Fix: Verification form submission wasn't working after returning from "choosing an alternate strategy" without making a selection. ([#3425](https://github.com/clerk/javascript/pull/3425)) by [@tmilewski](https://github.com/tmilewski) - - Perf: Adds a `NeverRetriable` state for applicable strategies so the countdown doesn't run needlessly. - -## 0.4.7 - -### Patch Changes - -- Update FieldError/GlobalError types to allow render function children while using the asChild prop ([#3426](https://github.com/clerk/javascript/pull/3426)) by [@tmilewski](https://github.com/tmilewski) - -## 0.4.6 - -## 0.4.5 - -### Patch Changes - -- Update `` to enable `asChild` prop for custom markup in render function usage. ([#3396](https://github.com/clerk/javascript/pull/3396)) by [@alexcarpenter](https://github.com/alexcarpenter) - -## 0.4.4 - -### Patch Changes - -- Fix `setActive` not firing upon a successful sign up. ([#3391](https://github.com/clerk/javascript/pull/3391)) by [@tmilewski](https://github.com/tmilewski) - -## 0.4.3 - -### Patch Changes - -- Fix typing for GlobalError and FieldError render functions ([#3387](https://github.com/clerk/javascript/pull/3387)) by [@tmilewski](https://github.com/tmilewski) - -## 0.4.2 - -## 0.4.1 - -### Patch Changes - -- This release includes various smaller fixes and one dependency update: ([#3343](https://github.com/clerk/javascript/pull/3343)) by [@tmilewski](https://github.com/tmilewski) - - `xstate` was updated from `5.12.0` to `5.13.0` - - Previously, the contents of the `fallback` prop were sometimes shown even if the user wasn't on the `start` step. This bug is fixed now. - - Upon completion of an sign-in/sign-up attempt, don't immediately return to the `start` step. This fixes the issue of a "flash of content" that could e.g. be seen during sign-in with OAuth providers. - - Some underlying fixes in Clerk Elements' XState logic were applied to make sure that during a sign-in/sign-up attempt the state is properly maintained. For example, if you visit an already completed attempt (some step of that flow) it now properly keeps track of that state. - -## 0.4.0 - -### Minor Changes - -- With this change `` and `` now render a `
`. This aligns them with all other `` components (which render an element, mostly ``). ([#3359](https://github.com/clerk/javascript/pull/3359)) by [@LekoArts](https://github.com/LekoArts) - - **Required action:** Update your markup to account for the new `
`, e.g. by removing an element you previously added yourself and moving props like `className` to the `` now. This change can be considered a breaking change so check if you're affected. - -## 0.3.3 - -### Patch Changes - -- The following are all internal changes and not relevant to any end-user: ([#3329](https://github.com/clerk/javascript/pull/3329)) by [@LauraBeatris](https://github.com/LauraBeatris) - - Create type interface for `TelemetryCollector` on `@clerk/types`. This allows to assign `telemetry` on the main Clerk SDK object, while inheriting from the actual `TelemetryCollector` implementation. - -- Refactors internal logic to avoid reliance on `useEffect`. This resolves potential for race conditions as a result of functionality coupled to component renders. ([#3320](https://github.com/clerk/javascript/pull/3320)) by [@tmilewski](https://github.com/tmilewski) - -- Typo fixes in README ([#3335](https://github.com/clerk/javascript/pull/3335)) by [@LekoArts](https://github.com/LekoArts) - -## 0.3.2 - -### Patch Changes - -- Fix issue where sign-up action resend would render type error for applying submit attribute ([#3327](https://github.com/clerk/javascript/pull/3327)) by [@alexcarpenter](https://github.com/alexcarpenter) - -## 0.3.1 - -### Patch Changes - -- Fix otp input overflow using clip path to prevent users clicking in the overflow space for password managers causing unexpected focus on input element ([#3317](https://github.com/clerk/javascript/pull/3317)) by [@alexcarpenter](https://github.com/alexcarpenter) - -## 0.3.0 - -### Minor Changes - -- Fix sign in route registration on development environments ([#3308](https://github.com/clerk/javascript/pull/3308)) by [@tmilewski](https://github.com/tmilewski) - -## 0.2.1 - -### Patch Changes - -- Add appropriate messaging for an invalid instance configuration vs implemented Elements strategies ([#3303](https://github.com/clerk/javascript/pull/3303)) by [@tmilewski](https://github.com/tmilewski) - -- Fixes a bug where multiple verification codes were sent at once. ([#3303](https://github.com/clerk/javascript/pull/3303)) by [@tmilewski](https://github.com/tmilewski) - -## 0.2.0 - -### Minor Changes - -- Bump version to 0.2.0 ([#3301](https://github.com/clerk/javascript/pull/3301)) by [@tmilewski](https://github.com/tmilewski) - - Fix return type of `` to be `JSX.Element | null` - -### Patch Changes - -- Update README to add install and usage instructions ([#3253](https://github.com/clerk/javascript/pull/3253)) by [@LekoArts](https://github.com/LekoArts) - -## 0.1.46 - -### Patch Changes - -- Consistently use sign-in/sign-up as a noun, sign in/sign up as a verb -- Change the `peerDependencies` range of `@clerk/clerk-react` and `@clerk/shared` to the stable Core 2 range -- Change the `peerDependency` range of `next` to mimic what `@clerk/nextjs` is doing -- Remove the `peerDependenciesMeta` entry for `@clerk/clerk-react` and `@clerk/shared` since that hacky workaround shouldn't be necessary anymore as Core 2 is stable now -- Change some imports of `@clerk/shared` to their subpath imports (e.g. `@clerk/shared/url`) to improve tree-shaking -- Add JSDoc comments where they were missing -- Update JSDoc comments to use namespace notation -- Misc JSDoc updates here and there - -## 0.1.45 - -### Patch Changes - -- Add the ability to use `` to support visible challenges - -## 0.1.44 - -### Patch Changes - -- Add `` / `` to Sign-Up - -## 0.1.43 - -### Patch Changes - -- **[BREAKING]** - - Rename `Provider` to `Connection` - - Rename `ProviderIcon` to `Icon` - - Update to handle both sign-up and sign-in - - export under `/common` - -## 0.1.42 - -### Patch Changes - -- Add Sign In Forgot Password functionality -- Clerk’s all-in-one components have a neat feature for the password input during sign-up: There's an instant validation (according to the dashboard password strength settings) and feedback on how good/bad the password is. - - You can add a `validatePassword` prop to `` to enable the aforementioned validation - - The `` component now returns beside `state` also `message` and `codes` - -## 0.1.41 - -### Patch Changes - -- Introduce `exampleMode`, which unconditionally renders the start step. In the future, we can leverage this to make "mock" functional flows in docs. We'll use this currently in the docs to showcase pre-built examples. - -## 0.1.40 - -### Patch Changes - -- Add resendable verifications to sign-in - -## 0.1.39 - -### Patch Changes - -- Add verifications warning/error messages when building a verifications flow - -## 0.1.38 - -### Patch Changes - -- Adjust types to prevent `asChild` usage when using the render prop for OTP input - -## 0.1.37 - -### Patch Changes - -- Change data attribute on OTP input to `data-otp-input-standard` - -## 0.1.36 - -### Patch Changes - -- Allow `className` on `` and `` - -## 0.1.35 - -### Patch Changes - -- Allow for controlled inputs - -## 0.1.34 - -### Patch Changes - -- Fix issues with TypeScript completions / IntelliSense - -## 0.1.33 - -### Patch Changes - -- Correctly passthrough `onChange` prop to `` - -## 0.1.32 - -### Patch Changes - -- Bugfixes for OTP component (``). You can now also pass a `passwordManagerOffset` prop to the component. It adds your specified number of pixels to the `width` of the input. - -## 0.1.31 - -### Patch Changes - -- Enable support for Next.js 14.1.0 or later (and its [window.history](https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate) changes) - -## 0.1.30 - -### Patch Changes - -- Ensure that `` can accept a `ref` - -## 0.1.29 - -### Patch Changes - -- Fix for a type issue with `` - -## 0.1.28 - -### Patch Changes - -- **[BREAKING]** - - The import for the `` component was moved to `/common` - ```diff - - import { Loading } from "@clerk/elements/sign-in" - - import { Loading } from "@clerk/elements/sign-up" - + import { Loading } from "@clerk/elements/common" - ``` - -## 0.1.27 - -### Patch Changes - -- **[BREAKING]** - - `` now returns its state directly in the function, not nested inside an object - - Before: `{({ state }) => ()}` - - After: `{state => ()}` -- `` now optionally allows for its children to accept a function providing the state. You don’t have to use the function, it’s optional. - - Example: `{state => ()}` - -## 0.1.26 - -### Patch Changes - -- Introduction of the `` component - -## 0.1.25 - -### Patch Changes - -- Removed internal usage of `"use client"` directive and replaced it with an implementation that still marks our components as client-only but throws a better error message when Clerk Elements is used in Server Components - -## 0.1.24 - -### Patch Changes - -- **[BREAKING]** - - `` was renamed to `` - - The `` component was removed. Use `` instead. - - Before: `Go back` - - After: `Go back` -- You can use `` now instead of the `` component - - Example: `Log in` - -## 0.1.23 - -### Patch Changes - -- Adds `` to SignIn, enabling the ability to display the current identifier being validated against. E.g.: `We’ve sent a temporary code to **.**` -- **[BREAKING]** Renames the following components: - - `` to `` - - `` to `` - -## 0.1.22 - -### Patch Changes - -- Disable debug logging by default. Go you can opt-in to it by using an environment variable: `NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG=1` - -## 0.1.21 - -### Patch Changes - -- Resolved installation issues with pnpm - -## 0.1.20 - -### Patch Changes - -- Temporarily removed Core 2 Beta peerDependencies to resolve npm issues. Once Core 2 is stable they’ll get added back - -## 0.1.19 - -### Patch Changes - -- **[BREAKING]** - - Require Core 2 Beta version to be used with Clerk Elements - - You can install the Next.js SDK with `npm install @clerk/nextjs@beta` - -## 0.1.18 - -### Patch Changes - -- Attempt to fix Strict Mode errors during development - -## 0.1.17 - -### Patch Changes - -- Fix incorrect peer dependencies - -## 0.1.16 - -### Patch Changes - -- Fix internal function that maps the `name` of the `` component to input types. Use the autocomplete/IntelliSense on `` to use supported names - -## 0.1.15 - -### Patch Changes - -- You can now provide alternative login strategies during sign in. It’s the “Use another method” functionality that Clerk’s current prebuilt components offer. - - Introduction of a `` component for the sign in flow. -- Introduction of a `` component. - -## 0.1.14 - -### Patch Changes - -- Performance improvements to underlying business logic -- **[BREAKING]** - - ``, `` and `` as export were removed. Use `` instead. - -## 0.1.13 - -### Patch Changes - -- Added JSDoc comments to all public APIs - -## 0.1.12 - -### Patch Changes - -- You can now add a `autoSubmit` prop to `` to automatically submit the form if the OTP input is complete - -## 0.1.11 - -### Patch Changes - -- Fix bugs in SAML/OAuth flows -- Add `` component which can be used instead of ``, `` and `` like this: - ```tsx - // You can also use name="continue" or name="verifications" - Contents - ``` - -## 0.1.10 - -### Patch Changes - -- Improved focus handling and accessibility of the `` component -- You can now pass a `length` prop to the ` - - - - - - -
-

@clerk/elements

-

- -
- -[![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_elements) -[![Follow on Twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) - -[Changelog](https://github.com/clerk/javascript/blob/main/packages/elements/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_elements) - -
- -## Getting started - -Clerk Elements is a library of unstyled, composable components that can be used to build completely custom UIs on top of Clerk's APIs, without having to manage the underlying logic. - -> [!WARNING] -> Clerk Elements is currently in beta. It's not recommended to use it in production just yet, but it would be much appreciated if you give it a try. -> If you have any feedback, please reach out to [beta-elements@clerk.dev](mailto:beta-elements@clerk.dev) or head over to the [GitHub Discussion](https://github.com/orgs/clerk/discussions/3315). - -### Prerequisites - -- Next.js `^13.5.4 || ^14.0.3` or later -- React 18 or later -- Node.js `>=18.17.0` or later -- Use the [Core 2 version](https://clerk.com/changelog/2024-04-19) (or later) of Clerk's SDKs -- An existing Clerk application. [Create your account for free](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_elements). - -### Installation - -The fastest way to get started with Clerk Elements is by following the [Clerk Elements "Getting started" guide](https://clerk.com/docs/customization/elements/overview#getting-started?utm_source=github&utm_medium=clerk_elements). - -## Usage - -For further information, guides, and examples visit the [Clerk Elements reference documentation](https://clerk.com/docs/customization/elements/overview?utm_source=github&utm_medium=clerk_elements). - -The following guides will show you how to build your own custom flows: - -- [Build a sign-in flow](https://clerk.com/docs/customization/elements/guides/sign-in?utm_source=github&utm_medium=clerk_elements) -- [Build a sign-up flow](https://clerk.com/docs/customization/elements/guides/sign-up?utm_source=github&utm_medium=clerk_elements) - -If you want to see what's possible with Clerk Elements, check out these pre-built examples from the Clerk team: - -- [Sign-in examples](https://clerk.com/docs/customization/elements/examples/sign-in?utm_source=github&utm_medium=clerk_elements) -- [Sign-up examples](https://clerk.com/docs/customization/elements/examples/sign-up?utm_source=github&utm_medium=clerk_elements) - -Finally, to learn about the available components and how to use them, check out the component reference pages: - -- [Common components](https://clerk.com/docs/customization/elements/reference/common?utm_source=github&utm_medium=clerk_elements) -- [Sign-in components](https://clerk.com/docs/customization/elements/reference/sign-in?utm_source=github&utm_medium=clerk_elements) -- [Sign-up components](https://clerk.com/docs/customization/elements/reference/sign-up?utm_source=github&utm_medium=clerk_elements) - -_With the beta release, only sign-up and sign-in flows are supported. Support for building the rest of Clerk's prebuilt components with Elements is actively being worked on._ - -## Support - -You can get in touch with us in any of the following ways: - -- Join our official community [Discord server](https://clerk.com/discord) -- On [our support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_elements) - -## 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/elements` follows good practices of security, but 100% security cannot be assured. - -`@clerk/elements` 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/elements/LICENSE) for more information. diff --git a/packages/elements/examples/nextjs/.env.local.example b/packages/elements/examples/nextjs/.env.local.example deleted file mode 100644 index e85051c07b3..00000000000 --- a/packages/elements/examples/nextjs/.env.local.example +++ /dev/null @@ -1,7 +0,0 @@ -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= -CLERK_SECRET_KEY= - -NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in -NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up -NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ -NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ diff --git a/packages/elements/examples/nextjs/.gitignore b/packages/elements/examples/nextjs/.gitignore deleted file mode 100644 index 27bf9049787..00000000000 --- a/packages/elements/examples/nextjs/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/packages/elements/examples/nextjs/README.md b/packages/elements/examples/nextjs/README.md deleted file mode 100644 index c4daa7315ea..00000000000 --- a/packages/elements/examples/nextjs/README.md +++ /dev/null @@ -1,28 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -### Development Process - -Please see the [package development documentation](../../README.md#package-development) for more information. - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/packages/elements/examples/nextjs/app/example/page.tsx b/packages/elements/examples/nextjs/app/example/page.tsx deleted file mode 100644 index a878a1a067f..00000000000 --- a/packages/elements/examples/nextjs/app/example/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; - -import * as Clerk from '@clerk/elements/common'; -import * as SignIn from '@clerk/elements/sign-in'; - -export default function ExamplePage() { - return ( - - -

Sign in

- - Email - - - - Sign in -
-
- ); -} diff --git a/packages/elements/examples/nextjs/app/favicon.ico b/packages/elements/examples/nextjs/app/favicon.ico deleted file mode 100644 index 718d6fea483..00000000000 Binary files a/packages/elements/examples/nextjs/app/favicon.ico and /dev/null differ diff --git a/packages/elements/examples/nextjs/app/globals.css b/packages/elements/examples/nextjs/app/globals.css deleted file mode 100644 index ec731e37102..00000000000 --- a/packages/elements/examples/nextjs/app/globals.css +++ /dev/null @@ -1,20 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-rgb: 214, 219, 220; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-rgb: 25, 26, 35; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: rgb(var(--background-rgb)); -} diff --git a/packages/elements/examples/nextjs/app/layout.tsx b/packages/elements/examples/nextjs/app/layout.tsx deleted file mode 100644 index 406ae495fbc..00000000000 --- a/packages/elements/examples/nextjs/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import './globals.css'; - -import { ClerkProvider } from '@clerk/nextjs'; -import { GeistMono } from 'geist/font/mono'; -import { GeistSans } from 'geist/font/sans'; -import type { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ); -} diff --git a/packages/elements/examples/nextjs/app/modal/page.tsx b/packages/elements/examples/nextjs/app/modal/page.tsx deleted file mode 100644 index 92e5edcfda2..00000000000 --- a/packages/elements/examples/nextjs/app/modal/page.tsx +++ /dev/null @@ -1,422 +0,0 @@ -'use client'; - -import * as Clerk from '@clerk/elements/common'; -import * as SignIn from '@clerk/elements/sign-in'; -import { SignedIn, SignedOut, SignOutButton } from '@clerk/nextjs'; -import * as Popover from '@radix-ui/react-popover'; -import Link from 'next/link'; -import { type ComponentProps, useState } from 'react'; - -import { H1, H3, P } from '@/components/design'; -import { CustomField } from '@/components/form'; -import { Spinner } from '@/components/spinner'; - -function CustomProvider({ - children, - provider, -}: { - children: string; - provider: ComponentProps['name']; -}) { - return ( - - {isLoading => ( - - - - {isLoading ? ( - <> - Loading... - - ) : ( - children - )} - - - )} - - ); -} - -function TextButton({ children, ...props }: ComponentProps<'button'>) { - return ( - - ); -} - -function Button({ children, ...props }: ComponentProps<'button'>) { - return ( - - ); -} - -function CustomSubmit({ children }: { children: string }) { - return ( - - {isLoading => (isLoading ? : children)} - - ); -} - -function ResendableFallback({ resendableAfter }: { resendableAfter: number }) { - return

Didn't recieve a code? Retry in {resendableAfter} seconds.

; -} - -function CustomResendable() { - return ( - - Didn't recieve a code? Retry Now - - ); -} - -export default function SignInPage() { - const [continueWithEmail, setContinueWithEmail] = useState(false); - - return ( -
- -
- - -

- Sign Out -

-
-
- - - - Open Sign In - - -
- - - - -
-

Sign In

-

- Don't have an account?{' '} - - Sign Up - -

-
-
- - -
- Continue with GitHub - Continue with Google -
- - {continueWithEmail ? ( - <> - - {fieldState => ( - <> - Email - - - - )} - - - Sign in with Email - - ) : ( - setContinueWithEmail(true)}>Continue with Email - )} -
-
- } - > -
-
-

Sign In

-

- Don't have an account?{' '} - - Sign Up - -

-
- -
- - {isLoading => Loading: {JSON.stringify(isLoading, null, 2)}} - -
- - -
- - -
- Continue with GitHub - Continue with Google -
- - {continueWithEmail ? ( - <> - - {fieldState => ( - <> - Email - - - - )} - - - Sign in with Email - - ) : ( - setContinueWithEmail(true)}>Continue with Email - )} -
-
- - -

CHOOSE STRATEGY:

- - Continue with GitHub - Continue with Google - - - - - - - - - - - - - - - Go back - -
- - -

FORGOT PASSWORD:

- - - - - - - - - -

Or

- - Continue with GitHub - Continue with Google - - - Go back - -
- - -
- - - -

- Welcome back ! -

- - - - Verify - - - Forgot Password - -
- - -

- Welcome back! We've sent a temporary code to -

- - - - - - Verify -
- - -

- Welcome back! We've sent a temporary code to -

- - - - - - Verify -
- - -

Verify your email

- -

- We've sent a verification code to -

- - - - Continue -
- - -

Verify your phone number

- -

- We've sent a verification code to -

- - - - Continue -
-
- - - Use another method - -
- - -
-

Reset your password

- -

Please reset your password to continue:

- - - Update Password -
-
-
- - - - - -
- ); -} diff --git a/packages/elements/examples/nextjs/app/otp-playground/page.tsx b/packages/elements/examples/nextjs/app/otp-playground/page.tsx deleted file mode 100644 index 7b1afd12bb7..00000000000 --- a/packages/elements/examples/nextjs/app/otp-playground/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; - -import { Field, Input, Label } from '@clerk/elements/common'; -import { SignIn, Step } from '@clerk/elements/sign-in'; -import clsx from 'clsx'; -import { AnimatePresence, motion } from 'framer-motion'; - -export default function Page() { - return ( - - -
- - - - - - ( -
- - {value && ( - - {value} - - )} - {value} - - - {/* {(status === 'cursor' || status === 'selected') && ( - - )} */} -
- )} - /> -
-
-
-
- ); -} diff --git a/packages/elements/examples/nextjs/app/page.tsx b/packages/elements/examples/nextjs/app/page.tsx deleted file mode 100644 index 2b5387cfe5d..00000000000 --- a/packages/elements/examples/nextjs/app/page.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { SignedIn, SignOutButton } from '@clerk/nextjs'; -import Image from 'next/image'; -import Link from 'next/link'; - -export default function Home() { - return ( -
-
-

- Get started by editing  - app/sign-in/[[...sign-in]]/page.tsx -

- - -

- Sign Out -

-
-
-
- -
- Clerk Logo -
- -
- -

- Sign-In Flow{' '} - - -> - -

-

Sign in using Elements

- - - -

- Sign Up Flow{' '} - - -> - -

-

Sign up using Elements

- - - -

- OTP{' '} - - -> - -

-

OTP Playground

- - - -

- Modal{' '} - - -> - -

-

Modal Playground

- - - - -

- Sessions{' '} - - -> - -

-

Choose from Active Sessions via Multi-session support

- -
- - -

- Clerk Docs{' '} - - -> - -

-

Clerk Custom Flow Sign-In Reference Docs

-
- - -

- XState 5 Docs{' '} - - -> - -

-

- Core XState 5 Documentation (used by Clerk Custom Flows) -

-
-
-
- ); -} diff --git a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx deleted file mode 100644 index d428a07f6de..00000000000 --- a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx +++ /dev/null @@ -1,506 +0,0 @@ -'use client'; - -import * as Clerk from '@clerk/elements/common'; -import * as SignIn from '@clerk/elements/sign-in'; -import Link from 'next/link'; -import { type ComponentProps, useState } from 'react'; - -import { H1, H3, P } from '@/components/design'; -import { CustomField } from '@/components/form'; -import { Spinner } from '@/components/spinner'; - -function CustomSamlConnection({ children }: { children: string }) { - return ( - - {isLoading => ( - - - {isLoading ? ( - <> - Loading... - - ) : ( - children - )} - - - )} - - ); -} - -function CustomProvider({ - children, - provider, -}: { - children: string; - provider: ComponentProps['name']; -}) { - return ( - - {isLoading => ( - - - - {isLoading ? ( - <> - Loading... - - ) : ( - children - )} - - - )} - - ); -} - -function TextButton({ children, ...props }: ComponentProps<'button'>) { - return ( - - ); -} - -function Button({ children, ...props }: ComponentProps<'button'>) { - return ( - - ); -} - -function CustomSubmit({ children }: { children: string }) { - return ( - - {isLoading => (isLoading ? : children)} - - ); -} - -function ResendableFallback({ resendableAfter }: { resendableAfter: number }) { - return

Didn't recieve a code? Retry in {resendableAfter} seconds.

; -} - -function CustomResendable() { - return ( - - Didn't recieve a code? Retry Now - - ); -} - -export default function SignInPage() { - const [continueWithEmail, setContinueWithEmail] = useState(false); - - return ( - -
-

Sign In

-

- Don't have an account?{' '} - - Sign Up - -

-
-
- - -
- Continue with GitHub - Continue with Google -
- - {continueWithEmail ? ( - <> - - {fieldState => ( - <> - Email - - - - )} - - -
- Sign in with Email - Continue with SAML -
- - ) : ( - setContinueWithEmail(true)}>Continue with Email - )} -
-
- } - > -
-
-

Sign In

-

- Don't have an account?{' '} - - Sign Up - -

-
- -
- {isLoading => Loading: {JSON.stringify(isLoading, null, 2)}} -
- - -
- - -
- Continue with GitHub - Continue with Google -
- - - {isLoading => (isLoading ? : 'Use passkey instead')} - - - {continueWithEmail ? ( - <> - - {fieldState => ( - <> - Email - - - - )} - - -
- Sign in with Email - Continue with SAML -
- - ) : ( - setContinueWithEmail(true)}>Continue with Email - )} -
-
- - -

CHOOSE SESSION:

- - -
- - {({ session }) => ( -

- {session.identifier} | Switch...{' '} -

- )} -
-
-
-
- -

CHOOSE STRATEGY:

- - Continue with GitHub - Continue with Google - - - - - - - - - - - - - - - - - - - - - - - - - - - Go back - -
- - -

FORGOT PASSWORD:

- - - - - - - - - -

Or

- - Continue with GitHub - Continue with Google - - - Go back - -
- - -
- - - -

- Welcome back ! -

- - Continue with Passkey -
- - -

- Welcome back ! -

- - - - Verify - - - Forgot Password - -
- - -

- Welcome back! We've sent a temporary code to -

- - - - - - Verify -
- - -

- Welcome back! We've sent a temporary code to -

- - - - - - Verify -
- - -

Please enter your authenticator code...

- - - - - - Verify -
- - -

Please enter your backup code...

- - - - - - Verify -
- - -

Verify your email

- -

- We've sent a verification code to -

- - - - Continue -
- - -

Verify your phone number

- -

- We've sent a verification code to -

- - - - Continue -
-
- - - Use another method - -
- - -
-

Reset your password

- -

Please reset your password to continue:

- - - Update Password -
-
- -
- - ); -} diff --git a/packages/elements/examples/nextjs/app/sign-up/[[...sign-up]]/page.tsx b/packages/elements/examples/nextjs/app/sign-up/[[...sign-up]]/page.tsx deleted file mode 100644 index 79c77e6b5ca..00000000000 --- a/packages/elements/examples/nextjs/app/sign-up/[[...sign-up]]/page.tsx +++ /dev/null @@ -1,238 +0,0 @@ -'use client'; - -import * as Clerk from '@clerk/elements/common'; -import * as SignUp from '@clerk/elements/sign-up'; -import Link from 'next/link'; -import type { ComponentProps } from 'react'; - -import { H1, HR as Hr, P } from '@/components/design'; -import { CustomField } from '@/components/form'; -import { Spinner } from '@/components/spinner'; - -function CustomSamlConnection({ children }: { children: string }) { - return ( - - {isLoading => ( - - - {isLoading ? ( - <> - Loading... - - ) : ( - children - )} - - - )} - - ); -} - -function CustomSubmit({ children }: ComponentProps<'button'>) { - return ( - - {isLoading => (isLoading ? : children)} - - ); -} - -function ResendableFallback({ resendableAfter }: { resendableAfter: number }) { - return

Didn't recieve a code? Retry in {resendableAfter} seconds.

; -} - -function CustomResendable() { - return ( - - Didn't recieve a code? Retry Now - - ); -} - -export default function SignUpPage() { - return ( - -
-

Sign Up

- -

- Have an account?{' '} - - Sign In - -

- -
- - - Sign In with GitHub - - - - - Sign In with Google - -
- -
- - - -
- - - - -
- Sign Up - Continue with SAML -
-
-
- - } - > -
- -
-

Sign Up

- -

- Have an account?{' '} - - Sign In - -

- -
- - - Sign In with GitHub - - - - - Sign In with Google - -
- -
- - - -
- - - - - - -
- Sign Up - Continue with SAML -
-
-
-
- - -

Please enter additional information:

- - - - - - Sign Up -
- - -

Verify your information:

- - - - - - - - - Verify - - - - - - - - Verify - - - - Please check your email for a link to verify your account. - - -
- - -

Restricted Access

-

Access to this app is limited, and an invitation is required to sign up.

-
-
-
- ); -} diff --git a/packages/elements/examples/nextjs/components/design.tsx b/packages/elements/examples/nextjs/components/design.tsx deleted file mode 100644 index 53470617662..00000000000 --- a/packages/elements/examples/nextjs/components/design.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { ComponentPropsWithoutRef } from 'react'; - -export const H1 = (props: ComponentPropsWithoutRef<'h1'>) => ( -

-); - -export const H2 = (props: ComponentPropsWithoutRef<'h2'>) => ( -

-); - -export const H3 = (props: ComponentPropsWithoutRef<'h3'>) => ( -

-); - -export const P = (props: ComponentPropsWithoutRef<'p'>) => ( -

-); - -export const HR = (props: ComponentPropsWithoutRef<'hr'>) => ( -


-); - -export function Button(props: React.ComponentProps<'button'>) { - return ( - - ) : null} - - - - - {({ state, codes, message }) => ( -
-
Field state: {state}
-
Field msg: {message}
- {name === 'password' ?
Pwd Keys: {codes?.join(', ')}
: null} -
- )} -
- - ); -}); - -const Field = CustomField; - -export { Field }; diff --git a/packages/elements/examples/nextjs/components/social-providers.tsx b/packages/elements/examples/nextjs/components/social-providers.tsx deleted file mode 100644 index e959fb0fb48..00000000000 --- a/packages/elements/examples/nextjs/components/social-providers.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { ProviderIcon as ClerkElementsProviderIcon } from '@clerk/elements/common'; -import Image from 'next/image'; -import type { ComponentProps } from 'react'; - -/** - * Helper component for easily circumventing Next's typing - * which requires `src`. It's being passed by the parent component. - */ -export const SocialProviderIcon = (props: ComponentProps) => ( - - {/* @ts-expect-error - required props are passed to child */} - - -); diff --git a/packages/elements/examples/nextjs/components/spinner.tsx b/packages/elements/examples/nextjs/components/spinner.tsx deleted file mode 100644 index d8b19d3f332..00000000000 --- a/packages/elements/examples/nextjs/components/spinner.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client'; - -export const Spinner = () => ( - - - - -); diff --git a/packages/elements/examples/nextjs/middleware.ts b/packages/elements/examples/nextjs/middleware.ts deleted file mode 100644 index 545508cedc1..00000000000 --- a/packages/elements/examples/nextjs/middleware.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clerkMiddleware } from '@clerk/nextjs/server'; -export default clerkMiddleware; - -export const config = { - matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], -}; diff --git a/packages/elements/examples/nextjs/next.config.js b/packages/elements/examples/nextjs/next.config.js deleted file mode 100644 index 840f438e060..00000000000 --- a/packages/elements/examples/nextjs/next.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - // Disable React strict mode when using the state machine inspector - reactStrictMode: false, - typescript: { - ignoreBuildErrors: true, - }, - eslint: { - ignoreDuringBuilds: true, - }, -}; - -module.exports = nextConfig; diff --git a/packages/elements/examples/nextjs/package.json b/packages/elements/examples/nextjs/package.json deleted file mode 100644 index f01c151b138..00000000000 --- a/packages/elements/examples/nextjs/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "elements-nextjs", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "next build", - "dev": "next dev", - "dev:debug": "NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG=true next dev", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "@clerk/elements": "file:../../elements", - "@clerk/nextjs": "file:../../nextjs", - "@radix-ui/react-form": "^0.1.8", - "@radix-ui/react-popover": "^1.1.15", - "clsx": "^2.0.0", - "framer-motion": "^11.0.28", - "geist": "^1.3.1", - "next": "14.2.33", - "react": "18.3.1", - "react-dom": "18.3.1" - }, - "devDependencies": { - "@types/node": "^18.19.130", - "@types/react": "catalog:react", - "@types/react-dom": "catalog:react", - "autoprefixer": "^10.4.21", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "typescript": "^5.8.3" - } -} diff --git a/packages/elements/examples/nextjs/postcss.config.js b/packages/elements/examples/nextjs/postcss.config.js deleted file mode 100644 index 12a703d900d..00000000000 --- a/packages/elements/examples/nextjs/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/packages/elements/examples/nextjs/public/clerk.svg b/packages/elements/examples/nextjs/public/clerk.svg deleted file mode 100644 index b4ba8eb3084..00000000000 --- a/packages/elements/examples/nextjs/public/clerk.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/elements/examples/nextjs/public/next.svg b/packages/elements/examples/nextjs/public/next.svg deleted file mode 100644 index 5174b28c565..00000000000 --- a/packages/elements/examples/nextjs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/elements/examples/nextjs/public/vercel.svg b/packages/elements/examples/nextjs/public/vercel.svg deleted file mode 100644 index d2f84222734..00000000000 --- a/packages/elements/examples/nextjs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/elements/examples/nextjs/tailwind.config.ts b/packages/elements/examples/nextjs/tailwind.config.ts deleted file mode 100644 index dd2f8e0e028..00000000000 --- a/packages/elements/examples/nextjs/tailwind.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Config } from 'tailwindcss'; - -const config: Config = { - content: [ - './pages/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - container: { - center: true, - }, - extend: { - colors: { - foreground: 'rgb(var(--foreground-rgb))', - background: 'rgb(var(--background-rgb))', - - secondary: 'rgba(0, 0, 0, 0.1)', - tertiary: 'rgba(255, 255, 255, 0.1)', - }, - fontFamily: { - sans: ['var(--font-geist-sans)'], - mono: ['var(--font-geist-mono)'], - }, - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - }, - }, - }, - plugins: [], -}; -export default config; diff --git a/packages/elements/examples/nextjs/tsconfig.json b/packages/elements/examples/nextjs/tsconfig.json deleted file mode 100644 index c7146963787..00000000000 --- a/packages/elements/examples/nextjs/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/packages/elements/jest.config.js b/packages/elements/jest.config.js deleted file mode 100644 index 42f0580aa8f..00000000000 --- a/packages/elements/jest.config.js +++ /dev/null @@ -1,22 +0,0 @@ -const { name } = require('./package.json'); -const { pathsToModuleNameMapper } = require('ts-jest'); -const { compilerOptions } = require('./tsconfig'); - -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - globals: { - PACKAGE_NAME: '@clerk/elements', - PACKAGE_VERSION: '0.0.0-test', - __DEV__: false, - }, - displayName: name.replace('@clerk', ''), - injectGlobals: true, - roots: [''], - testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'], - testEnvironment: 'jsdom', - transform: { '^.+\\.m?tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json' }] }, - setupFilesAfterEnv: ['/jest.setup.js'], - testPathIgnorePatterns: ['/node_modules/', '/jest/', '/.turbo', '/dist/', '/examples'], - modulePaths: [compilerOptions.baseUrl], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), -}; diff --git a/packages/elements/jest.setup.js b/packages/elements/jest.setup.js deleted file mode 100644 index 1a5dcdc343c..00000000000 --- a/packages/elements/jest.setup.js +++ /dev/null @@ -1 +0,0 @@ -process.env.CLERK_SECRET_KEY = 'TEST_SECRET_KEY'; diff --git a/packages/elements/package.json b/packages/elements/package.json deleted file mode 100644 index e0eb6c765aa..00000000000 --- a/packages/elements/package.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "name": "@clerk/elements", - "version": "0.23.89", - "description": "Clerk Elements", - "keywords": [ - "clerk", - "typescript", - "auth", - "authentication", - "passwordless", - "session", - "jwt", - "elements", - "radix" - ], - "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/elements" - }, - "license": "MIT", - "author": "Clerk", - "sideEffects": false, - "exports": { - "./*": { - "import": { - "types": "./dist/react/*/index.d.mts", - "default": "./dist/react/*/index.mjs" - }, - "require": { - "types": "./dist/react/*/index.d.ts", - "default": "./dist/react/*/index.js" - } - }, - ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - } - }, - "files": [ - "dist" - ], - "scripts": { - "app:build": "(cd examples/nextjs && pnpm build)", - "app:dev": "(cd examples/nextjs && pnpm dev)", - "app:dev:debug": "(cd examples/nextjs && NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG=true pnpm dev)", - "app:dev:debug:server": "(cd examples/nextjs && NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG=true CLERK_ELEMENTS_DEBUG_SERVER=true pnpm dev)", - "app:dev:debug:ui": "(cd examples/nextjs && NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG_UI=true pnpm dev)", - "app:e2e": "(cd examples/nextjs && pnpm e2e)", - "app:lint": "(cd examples/nextjs && pnpm lint)", - "build": "tsup --env.NODE_ENV production", - "build:analyze": "tsup --env.NODE_ENV production --metafile; open https://esbuild.github.io/analyze/", - "build:declarations": "tsc -p tsconfig.declarations.json", - "dev": "tsup --env.NODE_ENV development --watch", - "dev:example": "concurrently \"pnpm dev\" \"pnpm app:dev\"", - "format": "node ../../scripts/format-package.mjs", - "format:check": "node ../../scripts/format-package.mjs --check", - "lint": "eslint src", - "lint:attw": "attw --pack . --profile node16", - "lint:publint": "publint", - "test": "jest", - "test:cache:clear": "jest --clearCache --useStderr" - }, - "dependencies": { - "@clerk/clerk-react": "workspace:^", - "@clerk/shared": "workspace:^", - "@clerk/types": "workspace:^", - "@radix-ui/primitive": "^1.1.3", - "@radix-ui/react-form": "^0.1.8", - "@radix-ui/react-slot": "^1.2.3", - "@xstate/react": "^6.0.0", - "client-only": "^0.0.1", - "tslib": "catalog:repo", - "type-fest": "^4.41.0", - "xstate": "^5.20.2" - }, - "devDependencies": { - "@statelyai/inspect": "^0.4.0", - "concurrently": "^9.2.1", - "next": "14.2.33" - }, - "peerDependencies": { - "next": "^13.5.7 || ^14.2.25 || ^15.2.3 || ^16", - "react": "catalog:peer-react", - "react-dom": "catalog:peer-react" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } - }, - "engines": { - "node": ">=18.17.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/elements/src/index.ts b/packages/elements/src/index.ts deleted file mode 100644 index 1eb9b4dc1a7..00000000000 --- a/packages/elements/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -throw new Error(`No exports are available from the top-level "@clerk/elements" package. -Use specific subpath imports instead, e.g. "@clerk/elements/sign-in". - -Find all available exports in the documentation: -https://clerk.com/docs/elements/overview`); diff --git a/packages/elements/src/internals/constants/index.ts b/packages/elements/src/internals/constants/index.ts deleted file mode 100644 index 135bdfd35aa..00000000000 --- a/packages/elements/src/internals/constants/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { SignUpModes } from '@clerk/types'; - -import { safeAccess } from '~/utils/safe-access'; - -export const SSO_CALLBACK_PATH_ROUTE = '/sso-callback'; -export const CHOOSE_SESSION_PATH_ROUTE = '/choose'; -export const MAGIC_LINK_VERIFY_PATH_ROUTE = '/verify'; - -export const SIGN_UP_MODES: Record = { - PUBLIC: 'public', - RESTRICTED: 'restricted', -}; - -// TODO: remove reliance on next-specific variables here -export const SIGN_IN_DEFAULT_BASE_PATH = safeAccess( - () => process.env.CLERK_SIGN_IN_URL ?? process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL, - '/sign-in', -); -export const SIGN_UP_DEFAULT_BASE_PATH = safeAccess( - () => process.env.CLERK_SIGN_UP_URL ?? process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL, - '/sign-up', -); - -// The version that Next added support for the window.history.pushState and replaceState APIs. -// ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate -export const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; - -export const SEARCH_PARAMS = { - createdSession: '__clerk_created_session', - handshake: '__clerk_handshake', - help: '__clerk_help', - invitationToken: '__clerk_invitation_token', - modalState: '__clerk_modal_state', - satelliteUrl: '__clerk_satellite_url', - status: '__clerk_status', - synced: '__clerk_synced', - ticket: '__clerk_ticket', - transfer: '__clerk_transfer', -} as const; - -export const RESENDABLE_COUNTDOWN_DEFAULT = 60; - -export const CAPTCHA_ELEMENT_ID = 'clerk-captcha'; - -// Pulled from: https://github.com/clerk/javascript/blob/c7d626292a9fd12ca0f1b31a1035e711b6e99531/packages/clerk-js/src/core/constants.ts#L15 -export const ERROR_CODES = { - FORM_IDENTIFIER_NOT_FOUND: 'form_identifier_not_found', - FORM_PASSWORD_INCORRECT: 'form_password_incorrect', - INVALID_STRATEGY_FOR_USER: 'strategy_for_user_invalid', - NOT_ALLOWED_TO_SIGN_UP: 'not_allowed_to_sign_up', - OAUTH_ACCESS_DENIED: 'oauth_access_denied', - OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: 'oauth_email_domain_reserved_by_saml', - NOT_ALLOWED_ACCESS: 'not_allowed_access', - SAML_USER_ATTRIBUTE_MISSING: 'saml_user_attribute_missing', - USER_LOCKED: 'user_locked', - ENTERPRISE_SSO_USER_ATTRIBUTE_MISSING: 'enterprise_sso_user_attribute_missing', - ENTERPRISE_SSO_EMAIL_ADDRESS_DOMAIN_MISMATCH: 'enterprise_sso_email_address_domain_mismatch', - ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: 'enterprise_sso_hosted_domain_mismatch', - SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: 'saml_email_address_domain_mismatch', - ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: 'organization_membership_quota_exceeded_for_sso', -}; - -export const ROUTING = { - path: 'path', - virtual: 'virtual', - hash: 'hash', -} as const; - -export type ROUTING = (typeof ROUTING)[keyof typeof ROUTING]; diff --git a/packages/elements/src/internals/errors/index.ts b/packages/elements/src/internals/errors/index.ts deleted file mode 100644 index 74957c32861..00000000000 --- a/packages/elements/src/internals/errors/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import type { MetamaskError } from '@clerk/shared'; -import type { ClerkAPIError } from '@clerk/types'; - -export abstract class ClerkElementsErrorBase extends Error { - clerkError = true; - clerkElementsError = true; - rawMessage: string; - - constructor( - readonly code: string, - message: string, - ) { - super(message); - - this.name = 'ClerkElementsError'; - this.rawMessage = message; - } - - toString() { - return `[${this.name}]\nCode: ${this.code}\nMessage: ${this.message}`; - } -} - -export class ClerkElementsError extends ClerkElementsErrorBase { - static fromAPIError(error: ClerkAPIError | MetamaskError) { - return new ClerkElementsError( - error.code.toString(), - // @ts-expect-error - Expected that longMessage isn't a property of MetamaskError - error.longMessage || error.message, - ); - } - - constructor(code: string, message: string) { - super(code, message); - this.name = 'ClerkElementsError'; - } -} - -export class ClerkElementsRuntimeError extends ClerkElementsErrorBase { - constructor(message: string) { - super('elements_runtime_error', message); - this.name = 'ClerkElementsRuntimeError'; - } -} - -export class ClerkElementsFieldError extends ClerkElementsErrorBase { - static fromAPIError(error: ClerkAPIError) { - return new ClerkElementsFieldError(error.code, error.longMessage || error.message); - } - - constructor(code: string, message: string) { - super(code, message); - this.name = 'ClerkElementsFieldError'; - } - - get validityState() { - return this.code; - } - - get forceMatch() { - return true; - } - - matchFn = () => true; -} diff --git a/packages/elements/src/internals/machines/form/form.context.ts b/packages/elements/src/internals/machines/form/form.context.ts deleted file mode 100644 index 43948935e66..00000000000 --- a/packages/elements/src/internals/machines/form/form.context.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createActorContext } from '@xstate/react'; -import type { SnapshotFrom } from 'xstate'; - -import { FormMachine } from '~/internals/machines/form'; -import { inspect } from '~/internals/utils/inspector'; - -export type SnapshotState = SnapshotFrom; - -const FormMachineContext = createActorContext(FormMachine, { inspect }); - -export const FormStoreProvider = FormMachineContext.Provider; -export const useFormStore = FormMachineContext.useActorRef; -export const useFormSelector = FormMachineContext.useSelector; - -/** - * Selects a global error, if it exists - */ -export const globalErrorsSelector = (state: SnapshotState) => state.context.errors; - -/** - * Selects if a specific field has a value - */ -export const fieldValueSelector = (name: string | undefined) => (state: SnapshotState) => - name ? state.context.fields.get(name)?.value : ''; - -/** - * Selects if a specific field has a value - */ -export const fieldHasValueSelector = (name: string | undefined) => (state: SnapshotState) => - Boolean(fieldValueSelector(name)(state)); - -type MapValue = A extends Map ? V : never; - -/** - * Selects field-specific feedback, if they exist - * - * We declare an explicit return type here because TypeScript's inference results in the subtype reduction of the - * union used for feedback. Explicitly declaring the return type allows for all members of the union to be - * included in the return type. - */ -export const fieldFeedbackSelector = - (name: string | undefined) => - (state: SnapshotState): MapValue['feedback'] | undefined => - name ? state.context.fields.get(name)?.feedback : undefined; diff --git a/packages/elements/src/internals/machines/form/form.machine.ts b/packages/elements/src/internals/machines/form/form.machine.ts deleted file mode 100644 index 71c1cfb5bbc..00000000000 --- a/packages/elements/src/internals/machines/form/form.machine.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { isClerkAPIResponseError, isKnownError, isMetamaskError } from '@clerk/shared/error'; -import { snakeToCamel } from '@clerk/shared/underscore'; -import type { ClerkAPIError } from '@clerk/types'; -import type { MachineContext } from 'xstate'; -import { assign, enqueueActions, setup } from 'xstate'; - -import { ClerkElementsError, ClerkElementsFieldError } from '~/internals/errors'; - -import type { FieldDetails, FormDefaultValues, FormFields } from './form.types'; - -export interface FormMachineContext extends MachineContext { - defaultValues: FormDefaultValues; - errors: ClerkElementsError[]; - fields: FormFields; - hidden?: Set; - missing?: Set; - optional?: Set; - progressive: boolean; - required?: Set; -} - -export type FormMachineEvents = - | { type: 'FIELD.ADD'; field: Pick } - | { type: 'FIELD.REMOVE'; field: Pick } - | { type: 'FIELD.ENABLE'; field: Pick } - | { type: 'FIELD.DISABLE'; field: Pick } - | { - type: 'MARK_AS_PROGRESSIVE'; - defaultValues: FormDefaultValues; - missing: string[]; - optional: string[]; - required: string[]; - } - | { - type: 'PREFILL_DEFAULT_VALUES'; - defaultValues: FormDefaultValues; - } - | { type: 'UNMARK_AS_PROGRESSIVE' } - | { - type: 'FIELD.UPDATE'; - field: Pick; - } - | { type: 'ERRORS.SET'; error: any } - | { type: 'ERRORS.CLEAR' } - | { - type: 'FIELD.FEEDBACK.SET'; - field: Pick; - } - | { - type: 'FIELD.FEEDBACK.CLEAR'; - field: Pick; - } - | { type: 'FIELD.FEEDBACK.CLEAR.ALL' }; - -type FormMachineTypes = { - events: FormMachineEvents; - context: FormMachineContext; -}; - -export type TFormMachine = typeof FormMachine; - -/** - * A machine for managing form state. - * This machine is used alongside our other, flow-specific machines and a reference to a spawned FormMachine actor is used in the flows to interact with the form state. - */ -export const FormMachine = setup({ - actions: { - setGlobalErrors: assign({ - errors: (_, params: { errors: ClerkElementsError[] }) => [...params.errors], - }), - setFieldFeedback: assign({ - fields: ({ context }, params: Pick) => { - if (!params.name) { - throw new Error('Field name is required'); - } - - const fieldName = params.name; - if (context.fields.has(fieldName)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - context.fields.get(fieldName)!.feedback = params.feedback; - } - - return context.fields; - }, - }), - }, - types: {} as FormMachineTypes, -}).createMachine({ - id: 'Form', - context: () => ({ - defaultValues: new Map(), - errors: [], - fields: new Map(), - progressive: false, - }), - on: { - 'ERRORS.SET': { - actions: enqueueActions(({ enqueue, event }) => { - const isClerkAPIError = (err: any): err is ClerkAPIError => 'meta' in err; - - if (isKnownError(event.error)) { - const fields: Record = {}; - const globalErrors: ClerkElementsError[] = []; - const errors = isClerkAPIResponseError(event.error) ? event.error?.errors : [event.error]; - - for (const error of errors) { - const name = isClerkAPIError(error) ? snakeToCamel(error.meta?.paramName) : null; - - if (!name || isMetamaskError(error)) { - globalErrors.push(ClerkElementsError.fromAPIError(error)); - continue; - } - - if (!fields[name]) { - fields[name] = []; - } - - fields[name]?.push(ClerkElementsFieldError.fromAPIError(error)); - } - - enqueue({ - type: 'setGlobalErrors', - params: { - errors: globalErrors, - }, - }); - - for (const field in fields) { - enqueue({ - type: 'setFieldFeedback', - params: { - name: field, - feedback: { - type: 'error', - message: fields[field][0], - }, - }, - }); - } - } - }), - }, - 'ERRORS.CLEAR': { - actions: assign({ - errors: () => [], - }), - }, - 'FIELD.ADD': { - actions: assign({ - fields: ({ context, event }) => { - if (!event.field.name) { - throw new Error('Field name is required'); - } - - event.field.value = event.field.value || context.defaultValues.get(event.field.name) || undefined; - - context.fields.set(event.field.name, event.field); - return context.fields; - }, - }), - }, - 'FIELD.UPDATE': { - actions: assign({ - fields: ({ context, event }) => { - if (!event.field.name) { - throw new Error('Field name is required'); - } - - const field = context.fields.get(event.field.name); - - if (field) { - field.checked = event.field.checked; - field.disabled = event.field.disabled ?? field.disabled; - field.value = event.field.value; - - context.fields.set(event.field.name, field); - } - - return context.fields; - }, - }), - }, - 'FIELD.DISABLE': { - actions: assign({ - fields: ({ context, event }) => { - if (!event.field.name) { - throw new Error('Field name is required'); - } - - const field = context.fields.get(event.field.name); - - if (field) { - field.disabled = true; - context.fields.set(event.field.name, field); - } - - return context.fields; - }, - }), - }, - 'FIELD.ENABLE': { - actions: assign({ - fields: ({ context, event }) => { - if (!event.field.name) { - throw new Error('Field name is required'); - } - - const field = context.fields.get(event.field.name); - - if (field) { - field.disabled = false; - context.fields.set(event.field.name, field); - } - - return context.fields; - }, - }), - }, - 'FIELD.REMOVE': { - actions: assign({ - fields: ({ context, event }) => { - if (!event.field.name) { - throw new Error('Field name is required'); - } - - context.fields.delete(event.field.name); - return context.fields; - }, - }), - }, - 'FIELD.FEEDBACK.SET': { - actions: [ - { - type: 'setFieldFeedback', - params: ({ event }) => event.field, - }, - ], - }, - 'FIELD.FEEDBACK.CLEAR': { - actions: assign({ - fields: ({ context, event }) => { - if (!event.field.name) { - throw new Error('Field name is required'); - } - if (context.fields.has(event.field.name)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - context.fields.get(event.field.name)!.feedback = undefined; - } - - return context.fields; - }, - }), - }, - 'FIELD.FEEDBACK.CLEAR.ALL': { - actions: assign({ - fields: ({ context }) => { - context.fields.forEach(field => { - field.feedback = undefined; - }); - - return context.fields; - }, - }), - }, - MARK_AS_PROGRESSIVE: { - actions: assign(({ event }) => { - const missing = new Set(event.missing); - - return { - defaultValues: event.defaultValues, - hidden: new Set([...event.required.filter(f => !missing.has(f)), ...event.optional]), - missing, - optional: new Set(event.optional), - progressive: true, - required: new Set(event.required), - }; - }), - }, - UNMARK_AS_PROGRESSIVE: { - actions: assign({ - defaultValues: new Map(), - hidden: undefined, - missing: undefined, - optional: undefined, - progressive: false, - required: undefined, - }), - }, - PREFILL_DEFAULT_VALUES: { - actions: assign(({ event }) => { - return { - defaultValues: event.defaultValues, - }; - }), - }, - }, -}); diff --git a/packages/elements/src/internals/machines/form/form.types.ts b/packages/elements/src/internals/machines/form/form.types.ts deleted file mode 100644 index a5d4cfc5a7e..00000000000 --- a/packages/elements/src/internals/machines/form/form.types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ClerkElementsFieldError } from '~/internals/errors'; -import type { FieldStates } from '~/react/common/form/types'; -import type { PasswordConfig } from '~/react/hooks/use-password.hook'; -import type { ErrorCodeOrTuple } from '~/react/utils/generate-password-error-text'; - -export type FormDefaultValues = Map; - -interface FeedbackBase { - codes?: Array; -} - -export interface FeedbackErrorType extends FeedbackBase { - message: ClerkElementsFieldError; - type: Extract; -} - -export interface FeedbackOtherType extends FeedbackBase { - message: string; - type: Exclude; -} - -export interface FeedbackPasswordErrorType extends FeedbackErrorType { - config?: PasswordConfig; -} - -export interface FeedbackPasswordInfoType extends FeedbackOtherType { - config?: PasswordConfig; -} - -export type FieldDetails = { - checked?: boolean; - disabled?: boolean; - feedback?: FeedbackErrorType | FeedbackOtherType | FeedbackPasswordErrorType | FeedbackPasswordInfoType; - name?: string; - type: React.HTMLInputTypeAttribute; - value?: string | readonly string[] | number; -}; - -export type FormFields = Map; diff --git a/packages/elements/src/internals/machines/form/index.ts b/packages/elements/src/internals/machines/form/index.ts deleted file mode 100644 index 4d397cd4a26..00000000000 --- a/packages/elements/src/internals/machines/form/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './form.context'; -export * from './form.machine'; -export * from './form.types'; diff --git a/packages/elements/src/internals/machines/shared/__tests__/shared.actions.test.ts b/packages/elements/src/internals/machines/shared/__tests__/shared.actions.test.ts deleted file mode 100644 index ef65ac39e14..00000000000 --- a/packages/elements/src/internals/machines/shared/__tests__/shared.actions.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { sendToLoading } from '../shared.actions'; - -describe('sendToLoading', () => { - let context: any; - let event: any; - let parentSendMock: jest.Mock; - - beforeEach(() => { - context = { - parent: { - send: jest.fn(), - }, - loadingStep: new Error('Not implemented'), - }; - event = { - type: new Error('Not implemented'), - }; - parentSendMock = context.parent.send; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('should set loading state to false when event type starts with "xstate.done."', () => { - event.type = 'xstate.done.SOME_EVENT'; - context.loadingStep = 'start'; - - sendToLoading({ context, event }); - - expect(parentSendMock).toHaveBeenCalledWith({ - type: 'LOADING', - isLoading: false, - step: undefined, - strategy: undefined, - }); - }); - - test('should set loading state to false when event type starts with "xstate.error."', () => { - event.type = 'xstate.error.SOME_EVENT'; - context.loadingStep = 'start'; - - sendToLoading({ context, event }); - - expect(parentSendMock).toHaveBeenCalledWith({ - type: 'LOADING', - isLoading: false, - step: undefined, - strategy: undefined, - }); - }); - - test('should set loading state to true with undefined step and defined strategy when context.loadingStep is "strategy" and event.type is "REDIRECT"', () => { - context.loadingStep = 'strategy'; - event.type = 'REDIRECT'; - event.params = { - strategy: 'some-strategy', - }; - - sendToLoading({ context, event }); - - expect(parentSendMock).toHaveBeenCalledWith({ - type: 'LOADING', - isLoading: true, - step: undefined, - strategy: 'some-strategy', - }); - }); - - test('should set loading state to true with "continue" step and undefined strategy when loadingStep is "continue"', () => { - context.loadingStep = 'continue'; - event.type = 'SUBMIT'; - - sendToLoading({ context, event }); - - expect(parentSendMock).toHaveBeenCalledWith({ - type: 'LOADING', - isLoading: true, - step: 'continue', - strategy: undefined, - }); - }); - - test('should set loading state to true with the correct step and undefined strategy when loadingStep is not "strategy" or "continue"', () => { - context.loadingStep = 'some-step'; - event.type = 'SUBMIT'; - - sendToLoading({ context, event }); - - expect(parentSendMock).toHaveBeenCalledWith({ - type: 'LOADING', - isLoading: true, - step: 'some-step', - strategy: undefined, - }); - }); -}); diff --git a/packages/elements/src/internals/machines/shared/index.ts b/packages/elements/src/internals/machines/shared/index.ts deleted file mode 100644 index 77518a8a86b..00000000000 --- a/packages/elements/src/internals/machines/shared/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './shared.actions'; -export * from './shared.actors'; -export * from './shared.types'; diff --git a/packages/elements/src/internals/machines/shared/shared.actions.ts b/packages/elements/src/internals/machines/shared/shared.actions.ts deleted file mode 100644 index 33fd00af02d..00000000000 --- a/packages/elements/src/internals/machines/shared/shared.actions.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { SignInStrategy } from '@clerk/types'; - -import type { - SignInResetPasswordContext, - SignInResetPasswordEvents, - SignInStartContext, - SignInStartEvents, - SignInVerificationContext, - SignInVerificationEvents, -} from '~/internals/machines/sign-in'; -import type { - SignUpContinueContext, - SignUpContinueEvents, - SignUpStartContext, - SignUpStartEvents, - SignUpVerificationContext, - SignUpVerificationEvents, -} from '~/internals/machines/sign-up'; -import type { ThirdPartyMachineContext, ThirdPartyMachineEvent } from '~/internals/machines/third-party'; -import type { BaseRouterLoadingStep } from '~/internals/machines/types'; - -type SendToLoadingProps = { - context: - | SignInStartContext - | SignInVerificationContext - | SignInResetPasswordContext - | ThirdPartyMachineContext - | SignUpStartContext - | SignUpContinueContext - | SignUpVerificationContext; - event: - | SignInStartEvents - | SignInVerificationEvents - | SignInResetPasswordEvents - | ThirdPartyMachineEvent - | SignUpStartEvents - | SignUpContinueEvents - | SignUpVerificationEvents; -}; - -export function sendToLoading({ context, event }: SendToLoadingProps): void { - // Unrelated to the `context` of each machine, the step passed to the loading event must use BaseRouterLoadingStep - let step: BaseRouterLoadingStep | undefined; - let strategy: SignInStrategy | undefined; - let action: string | undefined; - - // By default the loading state is set to `true` when this function is called - // Only if these events are received, the loading state is set to `false` - // Early return here to avoid unnecessary checks - if (event.type.startsWith('xstate.done.') || event.type.startsWith('xstate.error.')) { - return context.parent.send({ - type: 'LOADING', - isLoading: false, - step: undefined, - strategy: undefined, - }); - } - - // `context.loadingStep: "strategy"` is not a valid BaseRouterLoadingStep (on purpose) so needs to be handled here. This context should be used when `step` should be undefined and `strategy` be defined instead - if (context.loadingStep === 'strategy') { - step = undefined; - - // Third-party machine handling - if (event.type === 'REDIRECT') { - strategy = event.params.strategy; - } - - return context.parent.send({ - type: 'LOADING', - isLoading: true, - step, - strategy, - }); - } else if (context.loadingStep === 'continue') { - step = 'continue'; - strategy = undefined; - action = 'action' in event ? event.action : undefined; - - return context.parent.send({ - type: 'LOADING', - isLoading: true, - step, - strategy, - action, - }); - } else if (context.loadingStep === 'reset-password') { - step = 'reset-password'; - strategy = undefined; - action = 'action' in event ? event.action : undefined; - - return context.parent.send({ - type: 'LOADING', - isLoading: true, - step, - strategy, - action, - }); - } else if (context.loadingStep === 'start') { - step = 'start'; - strategy = undefined; - action = 'action' in event ? event.action : undefined; - - return context.parent.send({ - type: 'LOADING', - isLoading: true, - step, - strategy, - action, - }); - } else { - step = context.loadingStep; - strategy = undefined; - action = 'action' in event ? event.action : undefined; - - return context.parent.send({ - type: 'LOADING', - isLoading: true, - step, - strategy, - action, - }); - } -} diff --git a/packages/elements/src/internals/machines/shared/shared.actors.ts b/packages/elements/src/internals/machines/shared/shared.actors.ts deleted file mode 100644 index 447ef67e32e..00000000000 --- a/packages/elements/src/internals/machines/shared/shared.actors.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Clerk, LoadedClerk } from '@clerk/types'; -import type { EventObject } from 'xstate'; -import { fromCallback } from 'xstate'; - -export type ClerkLoaderEvents = { type: 'CLERK.READY' } | { type: 'CLERK.ERROR'; message: string }; - -export const clerkLoader = fromCallback(({ sendBack, input: clerk }) => { - const reportLoaded = () => sendBack({ type: 'CLERK.READY' }); - - if (clerk.loaded) { - reportLoaded(); - } else if ('addOnLoaded' in clerk) { - // @ts-expect-error - Expects `addOnLoaded` from @clerk/shared/react's IsomorphicClerk. - clerk.addOnLoaded(reportLoaded); - } else { - sendBack({ type: 'ERROR', message: 'Clerk client could not be loaded' }); - } - - return () => {}; -}); diff --git a/packages/elements/src/internals/machines/shared/shared.types.ts b/packages/elements/src/internals/machines/shared/shared.types.ts deleted file mode 100644 index 6add45dfe40..00000000000 --- a/packages/elements/src/internals/machines/shared/shared.types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { - AuthenticateWithRedirectParams, - LoadedClerk, - OAuthStrategy, - SamlStrategy, - SignInStrategy, -} from '@clerk/types'; -import type { SetRequired, Simplify } from 'type-fest'; -import type { ActorRefFrom } from 'xstate'; - -import type { FormMachine } from '../form'; - -export type WithClerk> = { clerk: LoadedClerk } & T; -export type WithClient> = { client: LoadedClerk['client'] } & T; -export type WithParams = { params: T }; - -// ================= Unsafe Metadata ================= // - -export type WithUnsafeMetadata = T & { - unsafeMetadata?: SignUpUnsafeMetadata | undefined; -}; - -// ================= Authenticate With Redirect ================= // - -type SamlOnlyKeys = 'identifier' | 'emailAddress'; - -export type AuthenticateWithRedirectOAuthParams = Simplify< - Omit & { strategy: OAuthStrategy } ->; -export type AuthenticateWithRedirectSamlParams = Simplify< - SetRequired & { - strategy: SamlStrategy; - } ->; - -// ================= Strategies ================= // - -export type SignInStrategyName = SignInStrategy | 'oauth' | 'web3'; - -export type SetFormEvent = { type: 'SET_FORM'; formRef: ActorRefFrom }; diff --git a/packages/elements/src/internals/machines/sign-in/__tests__/router.selectors.test.ts b/packages/elements/src/internals/machines/sign-in/__tests__/router.selectors.test.ts deleted file mode 100644 index 36fe7661dfd..00000000000 --- a/packages/elements/src/internals/machines/sign-in/__tests__/router.selectors.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { SignInFirstFactor } from '@clerk/types'; - -import { SignInSafeIdentifierSelectorForStrategy } from '../router.selectors'; -import type { SignInRouterSnapshot } from '../router.types'; - -const IDENTIFIER = 'support@clerk.dev'; - -function createSnapshot( - supportedFirstFactors: Partial[] = [], - identifier?: string, -): SignInRouterSnapshot { - return { - context: { - clerk: { - client: { - signIn: { - status: 'needs_first_factor', - supportedFirstFactors: supportedFirstFactors.map(f => ({ - strategy: 'email_code', - emailAddressId: 'idn_foo', - ...f, - })), - supportedSecondFactors: null, - identifier: identifier, - }, - }, - }, - }, - } as unknown as SignInRouterSnapshot; -} - -describe('SignInSafeIdentifierSelectorForStrategy', () => { - describe('Match: Identifier', () => { - it('should output support@clerk.dev (matchingFactorForIdentifier.safeIdentifier)', () => { - const snapshot = createSnapshot( - [ - { - safeIdentifier: 's******@c****.com', - }, - { - safeIdentifier: IDENTIFIER, - primary: true, - }, - ], - IDENTIFIER, - ); - - const result = SignInSafeIdentifierSelectorForStrategy('email_code')(snapshot); - expect(result).toEqual(IDENTIFIER); - }); - }); - - describe('Match: Strategy', () => { - it('should output support@clerk.dev (matchingFactorForStrategy.safeIdentifier)', () => { - const snapshot = createSnapshot( - [ - { - safeIdentifier: 's******@c****.com', - }, - { - safeIdentifier: IDENTIFIER, - primary: true, - }, - ], - IDENTIFIER, - ); - - const result = SignInSafeIdentifierSelectorForStrategy('email_code')(snapshot); - expect(result).toEqual(IDENTIFIER); - }); - - it('should output s*****1@c****.com (matchingFactorForStrategy.safeIdentifier)', () => { - const snapshot = createSnapshot( - [ - { - safeIdentifier: 's*****1@c****.com', - }, - { - safeIdentifier: 's*****2@c****.com', - primary: true, - }, - ], - IDENTIFIER, - ); - - const result = SignInSafeIdentifierSelectorForStrategy('email_code')(snapshot); - expect(result).toEqual('s*****1@c****.com'); - }); - }); - - describe('Match: Default', () => { - it('should output support@clerk.dev (signIn.identifier)', () => { - const snapshot = createSnapshot([], IDENTIFIER); - const result = SignInSafeIdentifierSelectorForStrategy('email_code')(snapshot); - expect(result).toEqual(IDENTIFIER); - }); - - it('should output an empty string', () => { - const snapshot = createSnapshot([], undefined); - const result = SignInSafeIdentifierSelectorForStrategy('email_code')(snapshot); - expect(result).toEqual(''); - }); - }); -}); diff --git a/packages/elements/src/internals/machines/sign-in/index.ts b/packages/elements/src/internals/machines/sign-in/index.ts deleted file mode 100644 index 4aa5a84e817..00000000000 --- a/packages/elements/src/internals/machines/sign-in/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { SignInFirstFactorMachine, SignInSecondFactorMachine } from './verification.machine'; -export { SignInRouterMachine, SignInRouterMachineId } from './router.machine'; -export { SignInStartMachine, SignInStartMachineId } from './start.machine'; -export { SignInResetPasswordMachine, SignInResetPasswordMachineId } from './reset-password.machine'; - -export { SignInSafeIdentifierSelectorForStrategy, SignInSalutationSelector } from './router.selectors'; - -export type { TSignInRouterMachine } from './router.machine'; -export type { TSignInStartMachine } from './start.machine'; -export type { TSignInFirstFactorMachine, TSignInSecondFactorMachine } from './verification.machine'; -export type { TSignInResetPasswordMachine } from './reset-password.machine'; - -export * from './verification.types'; -export * from './router.types'; -export * from './start.types'; -export * from './reset-password.types'; diff --git a/packages/elements/src/internals/machines/sign-in/reset-password.machine.ts b/packages/elements/src/internals/machines/sign-in/reset-password.machine.ts deleted file mode 100644 index 7da26caae5a..00000000000 --- a/packages/elements/src/internals/machines/sign-in/reset-password.machine.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { SignInResource } from '@clerk/types'; -import type { DoneActorEvent } from 'xstate'; -import { fromPromise, sendTo, setup } from 'xstate'; - -import type { FormFields } from '~/internals/machines/form'; -import { sendToLoading } from '~/internals/machines/shared'; -import { assertActorEventError } from '~/internals/machines/utils/assert'; - -import type { SignInResetPasswordSchema } from './reset-password.types'; -import type { SignInRouterMachineActorRef } from './router.types'; - -export type TSignInResetPasswordMachine = typeof SignInResetPasswordMachine; - -export const SignInResetPasswordMachineId = 'SignInResetPasswordMachine'; - -export const SignInResetPasswordMachine = setup({ - actors: { - attempt: fromPromise( - ({ input: { fields, parent } }) => { - const password = (fields.get('password')?.value as string) || ''; - const signOutOfOtherSessions = fields.get('signOutOfOtherSessions')?.checked || false; - return parent.getSnapshot().context.clerk.client.signIn.resetPassword({ password, signOutOfOtherSessions }); - }, - ), - }, - actions: { - sendToLoading, - sendToNext: ({ context, event }) => - context.parent.send({ type: 'NEXT', resource: (event as unknown as DoneActorEvent).output }), - setFormErrors: sendTo( - ({ context }) => context.formRef, - ({ event }) => { - assertActorEventError(event); - return { - type: 'ERRORS.SET', - error: event.error, - }; - }, - ), - }, - types: {} as SignInResetPasswordSchema, -}).createMachine({ - id: SignInResetPasswordMachineId, - context: ({ input }) => ({ - loadingStep: 'reset-password', - parent: input.parent, - formRef: input.formRef, - }), - initial: 'Pending', - states: { - Pending: { - tags: ['state:pending'], - description: 'Waiting for user input', - on: { - SUBMIT: { - target: 'Attempting', - reenter: true, - }, - }, - }, - Attempting: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attempt', - src: 'attempt', - input: ({ context }) => ({ - parent: context.parent, - fields: context.formRef.getSnapshot().context.fields, - }), - onDone: { - actions: ['sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - }, -}); diff --git a/packages/elements/src/internals/machines/sign-in/reset-password.types.ts b/packages/elements/src/internals/machines/sign-in/reset-password.types.ts deleted file mode 100644 index a6b9b2d16ea..00000000000 --- a/packages/elements/src/internals/machines/sign-in/reset-password.types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ClerkAPIResponseError } from '@clerk/shared/error'; -import type { ActorRefFrom, ErrorActorEvent } from 'xstate'; - -import type { FormMachine } from '~/internals/machines/form'; - -import type { SignInRouterMachineActorRef } from './router.types'; - -// ---------------------------------- Tags ---------------------------------- // - -export type SignInResetPasswordTags = 'state:pending' | 'state:attempting' | 'state:loading'; - -// ---------------------------------- Events ---------------------------------- // - -export type SignInResetPasswordSubmitEvent = { type: 'SUBMIT'; action: 'submit' }; - -export type SignInResetPasswordEvents = ErrorActorEvent | SignInResetPasswordSubmitEvent; - -// ---------------------------------- Input ---------------------------------- // - -export type SignInResetPasswordInput = { - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; -}; - -// ---------------------------------- Context ---------------------------------- // - -export interface SignInResetPasswordContext { - error?: Error | ClerkAPIResponseError; - formRef: ActorRefFrom; - loadingStep: 'reset-password'; - parent: SignInRouterMachineActorRef; -} - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignInResetPasswordSchema { - context: SignInResetPasswordContext; - input: SignInResetPasswordInput; - events: SignInResetPasswordEvents; - tags: SignInResetPasswordTags; -} diff --git a/packages/elements/src/internals/machines/sign-in/router.machine.ts b/packages/elements/src/internals/machines/sign-in/router.machine.ts deleted file mode 100644 index d012894beb1..00000000000 --- a/packages/elements/src/internals/machines/sign-in/router.machine.ts +++ /dev/null @@ -1,658 +0,0 @@ -import { joinURL } from '@clerk/shared/url'; -import { isWebAuthnAutofillSupported } from '@clerk/shared/webauthn'; -import type { SignInStatus } from '@clerk/types'; -import type { NonReducibleUnknown } from 'xstate'; -import { and, assign, enqueueActions, fromPromise, log, not, or, raise, sendTo, setup } from 'xstate'; - -import { - CHOOSE_SESSION_PATH_ROUTE, - ERROR_CODES, - ROUTING, - SEARCH_PARAMS, - SIGN_IN_DEFAULT_BASE_PATH, - SIGN_UP_DEFAULT_BASE_PATH, - SSO_CALLBACK_PATH_ROUTE, -} from '~/internals/constants'; -import { ClerkElementsError, ClerkElementsRuntimeError } from '~/internals/errors'; -import { ThirdPartyMachine, ThirdPartyMachineId } from '~/internals/machines/third-party'; -import { shouldUseVirtualRouting } from '~/internals/machines/utils/next'; - -import { FormMachine } from '../form'; -import { SignInResetPasswordMachine } from './reset-password.machine'; -import type { - SignInRouterContext, - SignInRouterEvents, - SignInRouterNextEvent, - SignInRouterSchema, - SignInRouterSessionSetActiveEvent, -} from './router.types'; -import { SignInStartMachine } from './start.machine'; -import { SignInFirstFactorMachine, SignInSecondFactorMachine } from './verification.machine'; - -export type TSignInRouterMachine = typeof SignInRouterMachine; - -const isCurrentPath = - (path: `/${string}`) => - ({ context }: { context: SignInRouterContext }, _params?: NonReducibleUnknown) => { - return context.router?.match(path) ?? false; - }; - -const needsStatus = - (status: SignInStatus) => - ({ context, event }: { context: SignInRouterContext; event?: SignInRouterEvents }, _?: NonReducibleUnknown) => - (event as SignInRouterNextEvent)?.resource?.status === status || context.clerk?.client.signIn.status === status; - -export const SignInRouterMachineId = 'SignInRouter'; - -export const SignInRouterMachine = setup({ - actors: { - firstFactorMachine: SignInFirstFactorMachine, - formMachine: FormMachine, - resetPasswordMachine: SignInResetPasswordMachine, - startMachine: SignInStartMachine, - secondFactorMachine: SignInSecondFactorMachine, - thirdPartyMachine: ThirdPartyMachine, - webAuthnAutofillSupport: fromPromise(() => isWebAuthnAutofillSupported()), - }, - actions: { - clearFormErrors: sendTo(({ context }) => context.formRef, { type: 'ERRORS.CLEAR' }), - navigateInternal: ({ context }, { path, force = false }: { path: string; force?: boolean }) => { - if (!context.router) { - return; - } - if (!force && shouldUseVirtualRouting()) { - return; - } - if (context.exampleMode) { - return; - } - - const resolvedPath = joinURL(context.router.basePath, path); - if (resolvedPath === context.router.pathname()) { - return; - } - - context.router.shallowPush(resolvedPath); - }, - navigateExternal: ({ context }, { path }: { path: string }) => context.router?.push(path), - raiseNext: raise({ type: 'NEXT' }), - setActive: enqueueActions(({ enqueue, check, context, event }) => { - if (check('isExampleMode')) { - return; - } - - const id = (event as SignInRouterSessionSetActiveEvent)?.id; - const lastActiveSessionId = context.clerk.client.lastActiveSessionId; - const createdSessionId = ((event as SignInRouterNextEvent)?.resource || context.clerk.client.signIn) - .createdSessionId; - - const session = id || createdSessionId || lastActiveSessionId || null; - - void context.clerk.setActive({ - session, - redirectUrl: context.clerk.buildAfterSignInUrl({ - params: context.router?.searchParams(), - }), - }); - - enqueue.raise({ type: 'RESET' }, { delay: 2000 }); // Reset machine after 2s delay. - }), - setError: assign({ - error: (_, { error }: { error?: ClerkElementsError }) => { - if (error) { - return error; - } - return new ClerkElementsRuntimeError('Unknown error'); - }, - }), - setFormErrors: ({ context }, params: { error: Error }) => - sendTo(context.formRef, { - type: 'ERRORS.SET', - error: params.error, - }), - setFormOAuthErrors: ({ context }) => { - const errorOrig = context.clerk.client.signIn.firstFactorVerification.error; - - if (!errorOrig) { - return; - } - - let error: ClerkElementsError; - - switch (errorOrig.code) { - case ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP: - case ERROR_CODES.OAUTH_ACCESS_DENIED: - case ERROR_CODES.NOT_ALLOWED_ACCESS: - case ERROR_CODES.SAML_USER_ATTRIBUTE_MISSING: - case ERROR_CODES.OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: - case ERROR_CODES.USER_LOCKED: - case ERROR_CODES.ENTERPRISE_SSO_USER_ATTRIBUTE_MISSING: - case ERROR_CODES.ENTERPRISE_SSO_EMAIL_ADDRESS_DOMAIN_MISMATCH: - case ERROR_CODES.ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: - case ERROR_CODES.SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: - case ERROR_CODES.ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: - error = new ClerkElementsError(errorOrig.code, errorOrig.longMessage || ''); - break; - default: - error = new ClerkElementsError( - 'unable_to_complete', - 'Unable to complete action at this time. If the problem persists please contact support.', - ); - } - - context.formRef.send({ - type: 'ERRORS.SET', - error, - }); - }, - transfer: ({ context }) => { - const searchParams = new URLSearchParams({ __clerk_transfer: '1' }); - context.router?.push(`${context.signUpPath}?${searchParams}`); - }, - }, - guards: { - hasAuthenticatedViaClerkJS: ({ context }) => - Boolean(context.clerk.client.signIn.status === null && context.clerk.client.lastActiveSessionId), - hasOAuthError: ({ context }) => Boolean(context.clerk?.client?.signIn?.firstFactorVerification?.error), - hasResource: ({ context }) => Boolean(context.clerk?.client?.signIn?.status), - hasTicket: ({ context }) => Boolean(context.ticket), - - isLoggedInAndSingleSession: and(['isLoggedIn', 'isSingleSessionMode', not('isExampleMode')]), - isActivePathRoot: isCurrentPath('/'), - isComplete: ({ context, event }) => { - const resource = (event as SignInRouterNextEvent)?.resource; - const signIn = context.clerk.client.signIn; - - return ( - (resource?.status === 'complete' && Boolean(resource?.createdSessionId)) || - (signIn.status === 'complete' && Boolean(signIn.createdSessionId)) - ); - }, - isLoggedIn: ({ context }) => Boolean(context.clerk?.user), - isSingleSessionMode: ({ context }) => Boolean(context.clerk?.__unstable__environment?.authConfig.singleSessionMode), - isExampleMode: ({ context }) => Boolean(context.exampleMode), - - needsStart: or([not('hasResource'), 'statusNeedsIdentifier', isCurrentPath('/')]), - needsFirstFactor: and(['statusNeedsFirstFactor', isCurrentPath('/continue')]), - needsSecondFactor: and(['statusNeedsSecondFactor', isCurrentPath('/continue')]), - needsCallback: isCurrentPath(SSO_CALLBACK_PATH_ROUTE), - needsChooseSession: isCurrentPath(CHOOSE_SESSION_PATH_ROUTE), - needsNewPassword: and(['statusNeedsNewPassword', isCurrentPath('/new-password')]), - - statusNeedsIdentifier: needsStatus('needs_identifier'), - statusNeedsFirstFactor: needsStatus('needs_first_factor'), - statusNeedsSecondFactor: needsStatus('needs_second_factor'), - statusNeedsNewPassword: needsStatus('needs_new_password'), - }, - types: {} as SignInRouterSchema, -}).createMachine({ - id: SignInRouterMachineId, - // @ts-expect-error - Set in INIT event - context: {}, - initial: 'Idle', - on: { - 'AUTHENTICATE.OAUTH': { - actions: sendTo(ThirdPartyMachineId, ({ context, event }) => ({ - type: 'REDIRECT', - params: { - strategy: event.strategy, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signInUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, - redirectUrlComplete: context.clerk.buildAfterSignInUrl({ - params: context.router?.searchParams(), - }), - }, - })), - }, - 'AUTHENTICATE.SAML': { - actions: sendTo(ThirdPartyMachineId, ({ context }) => ({ - type: 'REDIRECT', - params: { - strategy: 'saml', - identifier: context.formRef.getSnapshot().context.fields.get('identifier')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signInUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, - redirectUrlComplete: context.clerk.buildAfterSignInUrl({ - params: context.router?.searchParams(), - }), - }, - })), - }, - 'AUTHENTICATE.ENTERPRISE_SSO': { - actions: sendTo(ThirdPartyMachineId, ({ context }) => ({ - type: 'REDIRECT', - params: { - strategy: 'enterprise_sso', - identifier: context.formRef.getSnapshot().context.fields.get('identifier')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signInUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, - redirectUrlComplete: context.clerk.buildAfterSignInUrl({ - params: context.router?.searchParams(), - }), - }, - })), - }, - 'FORM.ATTACH': { - description: 'Attach/re-attach the form to the router.', - actions: enqueueActions(({ enqueue, event }) => { - enqueue.assign({ - formRef: event.formRef, - }); - - // Reset the current step, to reset the form reference. - enqueue.raise({ type: 'RESET.STEP' }); - }), - }, - 'NAVIGATE.PREVIOUS': '.Hist', - 'NAVIGATE.START': '.Start', - LOADING: { - actions: assign(({ event }) => ({ - loading: { - isLoading: event.isLoading, - step: event.step, - strategy: event.strategy, - action: event.action, - }, - })), - }, - RESET: '.Idle', - }, - states: { - Idle: { - invoke: { - id: 'webAuthnAutofill', - src: 'webAuthnAutofillSupport', - onDone: { - actions: assign({ webAuthnAutofillSupport: ({ event }) => event.output }), - }, - }, - on: { - INIT: { - actions: assign(({ event }) => { - const searchParams = event.router?.searchParams(); - - return { - clerk: event.clerk, - exampleMode: event.exampleMode || false, - formRef: event.formRef, - loading: { - isLoading: false, - }, - router: event.router, - signUpPath: event.signUpPath || SIGN_UP_DEFAULT_BASE_PATH, - ticket: searchParams?.get(SEARCH_PARAMS.ticket) || undefined, - }; - }), - target: 'Init', - }, - }, - }, - Init: { - entry: enqueueActions(({ context, enqueue, self }) => { - if (!self.getSnapshot().children[ThirdPartyMachineId]) { - enqueue.spawnChild('thirdPartyMachine', { - id: ThirdPartyMachineId, - systemId: ThirdPartyMachineId, - input: { - basePath: context.router?.basePath ?? SIGN_IN_DEFAULT_BASE_PATH, - flow: 'signIn', - formRef: context.formRef, - parent: self, - }, - }); - } - }), - always: [ - { - guard: 'needsCallback', - target: 'Callback', - }, - { - guard: 'needsChooseSession', - target: 'ChooseSession', - }, - { - guard: 'isComplete', - actions: 'setActive', - }, - { - guard: 'isLoggedInAndSingleSession', - actions: [ - log('Already logged in'), - { - type: 'navigateExternal', - params: ({ context }) => ({ - path: context.clerk.buildAfterSignInUrl({ - params: context.router?.searchParams(), - }), - }), - }, - ], - }, - { - guard: 'needsStart', - actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, - target: 'Start', - }, - { - guard: 'needsFirstFactor', - actions: { type: 'navigateInternal', params: { force: true, path: '/continue' } }, - target: 'FirstFactor', - }, - { - guard: 'needsSecondFactor', - actions: { type: 'navigateInternal', params: { force: true, path: '/continue' } }, - target: 'SecondFactor', - }, - { - guard: 'needsNewPassword', - actions: { type: 'navigateInternal', params: { force: true, path: '/reset-password' } }, - target: 'ResetPassword', - }, - { - actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, - target: 'Start', - }, - { - guard: 'hasTicket', - actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, - target: 'Start', - }, - ], - }, - Start: { - tags: ['step:start'], - exit: 'clearFormErrors', - invoke: { - id: 'start', - src: 'startMachine', - input: ({ context, self }) => ({ - basePath: context.router?.basePath, - formRef: context.formRef, - parent: self, - ticket: context.ticket, - }), - onDone: { - actions: 'raiseNext', - }, - }, - on: { - 'RESET.STEP': { - target: 'Start', - reenter: true, - }, - 'AUTHENTICATE.PASSKEY': { - actions: sendTo('start', ({ event }) => event), - }, - 'AUTHENTICATE.PASSKEY.AUTOFILL': { - actions: sendTo('start', ({ event }) => event), - }, - 'AUTHENTICATE.WEB3': { - actions: sendTo('start', ({ event }) => event), - }, - NEXT: [ - { - guard: 'isComplete', - actions: 'setActive', - }, - { - guard: 'statusNeedsFirstFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'FirstFactor', - }, - { - guard: 'statusNeedsSecondFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'SecondFactor', - }, - { - guard: 'statusNeedsNewPassword', - actions: { type: 'navigateInternal', params: { path: '/reset-password' } }, - target: 'ResetPassword', - }, - ], - }, - }, - FirstFactor: { - tags: ['step:first-factor', 'step:verifications'], - invoke: { - id: 'firstFactor', - src: 'firstFactorMachine', - input: ({ context, self }) => ({ - formRef: context.formRef, - parent: self, - basePath: context.router?.basePath, - }), - onDone: { - actions: 'raiseNext', - }, - }, - on: { - 'AUTHENTICATE.PASSKEY': { - actions: sendTo('firstFactor', ({ event }) => event), - }, - 'RESET.STEP': { - target: 'FirstFactor', - reenter: true, - }, - NEXT: [ - { - guard: 'isComplete', - actions: 'setActive', - }, - { - guard: 'statusNeedsSecondFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'SecondFactor', - }, - { - guard: 'statusNeedsNewPassword', - actions: { type: 'navigateInternal', params: { path: '/reset-password' } }, - target: 'ResetPassword', - }, - ], - 'STRATEGY.UPDATE': { - description: 'Send event to verification machine to update the current strategy.', - actions: sendTo('firstFactor', ({ event }) => event), - target: '.Idle', - }, - }, - initial: 'Idle', - states: { - Idle: { - on: { - 'NAVIGATE.FORGOT_PASSWORD': { - description: 'Navigate to forgot password screen.', - actions: sendTo('firstFactor', ({ event }) => event), - target: 'ForgotPassword', - }, - 'NAVIGATE.CHOOSE_STRATEGY': { - description: 'Navigate to choose strategy screen.', - actions: sendTo('firstFactor', ({ event }) => event), - target: 'ChoosingStrategy', - }, - }, - }, - ChoosingStrategy: { - tags: ['step:choose-strategy'], - on: { - 'NAVIGATE.PREVIOUS': { - description: 'Go to Idle, and also tell firstFactor to go to Pending', - target: 'Idle', - actions: sendTo('firstFactor', { type: 'NAVIGATE.PREVIOUS' }), - }, - }, - }, - ForgotPassword: { - tags: ['step:forgot-password'], - on: { - 'NAVIGATE.PREVIOUS': 'Idle', - }, - }, - }, - }, - SecondFactor: { - tags: ['step:second-factor', 'step:verifications'], - invoke: { - id: 'secondFactor', - src: 'secondFactorMachine', - input: ({ context, self }) => ({ - formRef: context.formRef, - parent: self, - }), - onDone: { - actions: 'raiseNext', - }, - }, - on: { - 'RESET.STEP': { - target: 'SecondFactor', - reenter: true, - }, - NEXT: [ - { - guard: 'isComplete', - actions: 'setActive', - }, - { - guard: 'statusNeedsNewPassword', - actions: { type: 'navigateInternal', params: { path: '/reset-password' } }, - target: 'ResetPassword', - }, - ], - 'STRATEGY.UPDATE': { - description: 'Send event to verification machine to update the current strategy.', - actions: sendTo('secondFactor', ({ event }) => event), - target: '.Idle', - }, - }, - initial: 'Idle', - states: { - Idle: { - on: { - 'NAVIGATE.CHOOSE_STRATEGY': { - description: 'Navigate to choose strategy screen.', - actions: sendTo('secondFactor', ({ event }) => event), - target: 'ChoosingStrategy', - }, - }, - }, - ChoosingStrategy: { - tags: ['step:choose-strategy'], - on: { - 'NAVIGATE.PREVIOUS': { - description: 'Go to Idle, and also tell firstFactor to go to Pending', - target: 'Idle', - actions: sendTo('secondFactor', { type: 'NAVIGATE.PREVIOUS' }), - }, - }, - }, - }, - }, - ResetPassword: { - tags: ['step:reset-password'], - invoke: { - id: 'resetPassword', - src: 'resetPasswordMachine', - input: ({ context, self }) => ({ - formRef: context.formRef, - parent: self, - }), - onDone: { - actions: 'raiseNext', - }, - }, - on: { - 'RESET.STEP': { - target: 'ResetPassword', - reenter: true, - }, - NEXT: [ - { - guard: 'isComplete', - actions: 'setActive', - }, - { - guard: 'statusNeedsFirstFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'FirstFactor', - }, - { - guard: 'statusNeedsSecondFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'SecondFactor', - }, - ], - }, - }, - Callback: { - tags: ['step:callback'], - entry: sendTo(ThirdPartyMachineId, { type: 'CALLBACK' }), - on: { - NEXT: [ - { - guard: 'hasOAuthError', - actions: ['setFormOAuthErrors', { type: 'navigateInternal', params: { force: true, path: '/' } }], - target: 'Start', - }, - { - guard: or(['isLoggedIn', 'isComplete', 'hasAuthenticatedViaClerkJS']), - actions: 'setActive', - }, - { - guard: 'statusNeedsIdentifier', - actions: 'transfer', - }, - { - guard: 'statusNeedsFirstFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'FirstFactor', - }, - { - guard: 'statusNeedsSecondFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'SecondFactor', - }, - { - guard: 'statusNeedsNewPassword', - actions: { type: 'navigateInternal', params: { path: '/reset-password' } }, - target: 'ResetPassword', - }, - ], - }, - }, - ChooseSession: { - tags: ['step:choose-session'], - on: { - 'SESSION.SET_ACTIVE': { - actions: { - type: 'setActive', - params: ({ event }) => ({ id: event.id }), - }, - }, - }, - }, - Error: { - tags: ['step:error'], - on: { - NEXT: { - target: 'Start', - actions: 'clearFormErrors', - }, - }, - }, - Hist: { - type: 'history', - exit: 'clearFormErrors', - }, - }, -}); diff --git a/packages/elements/src/internals/machines/sign-in/router.selectors.ts b/packages/elements/src/internals/machines/sign-in/router.selectors.ts deleted file mode 100644 index 801c78d835f..00000000000 --- a/packages/elements/src/internals/machines/sign-in/router.selectors.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { SignInStrategyName } from '~/internals/machines/shared'; -import { formatSalutation } from '~/internals/machines/utils/formatters'; - -import type { SignInRouterSnapshot } from './router.types'; - -export function SignInSafeIdentifierSelectorForStrategy( - strategy: SignInStrategyName | undefined, -): (s: SignInRouterSnapshot) => string { - return (s: SignInRouterSnapshot) => { - const signIn = s.context.clerk?.client.signIn; - - if (strategy) { - const matchingFactors = [ - ...(signIn.supportedFirstFactors ?? []), - ...(signIn.supportedSecondFactors ?? []), - ].filter(f => f.strategy === strategy); - - const matchingFactorForIdentifier = - signIn.identifier && matchingFactors.length > 0 - ? matchingFactors.find(f => 'safeIdentifier' in f && f.safeIdentifier === signIn.identifier) - : null; - - const matchingFactorForStrategy = matchingFactors[0]; - - if (matchingFactorForIdentifier && 'safeIdentifier' in matchingFactorForIdentifier) { - return matchingFactorForIdentifier.safeIdentifier; - } - - if (matchingFactorForStrategy && 'safeIdentifier' in matchingFactorForStrategy) { - return matchingFactorForStrategy.safeIdentifier; - } - } - - return signIn.identifier || ''; - }; -} - -export function SignInSalutationSelector(s: SignInRouterSnapshot): string { - const signIn = s.context.clerk?.client.signIn; - - return formatSalutation({ - firstName: signIn?.userData?.firstName, - identifier: signIn?.identifier, - lastName: signIn?.userData?.lastName, - }); -} diff --git a/packages/elements/src/internals/machines/sign-in/router.types.ts b/packages/elements/src/internals/machines/sign-in/router.types.ts deleted file mode 100644 index d084c173f18..00000000000 --- a/packages/elements/src/internals/machines/sign-in/router.types.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { SignInResource } from '@clerk/types'; -import type { ActorRefFrom, MachineSnapshot, StateMachine } from 'xstate'; - -import type { TFormMachine } from '~/internals/machines/form'; -import type { - BaseRouterContext, - BaseRouterErrorEvent, - BaseRouterFormAttachEvent, - BaseRouterInput, - BaseRouterLoadingEvent, - BaseRouterNextEvent, - BaseRouterPrevEvent, - BaseRouterRedirectEvent, - BaseRouterResetEvent, - BaseRouterResetStepEvent, - BaseRouterSetClerkEvent, - BaseRouterStartEvent, - BaseRouterTransferEvent, -} from '~/internals/machines/types'; - -import type { SignInVerificationFactorUpdateEvent } from './verification.types'; - -// ---------------------------------- Tags ---------------------------------- // - -export const SignInRouterSteps = { - start: 'step:start', - verifications: 'step:verifications', - firstFactor: 'step:first-factor', - secondFactor: 'step:second-factor', - callback: 'step:callback', - error: 'step:error', - forgotPassword: 'step:forgot-password', - resetPassword: 'step:reset-password', - chooseSession: 'step:choose-session', - chooseStrategy: 'step:choose-strategy', -} as const; - -export type SignInRouterSteps = keyof typeof SignInRouterSteps; -export type SignInRouterTags = (typeof SignInRouterSteps)[keyof typeof SignInRouterSteps]; - -// ---------------------------------- Children ---------------------------------- // - -export const SignInRouterSystemId = { - start: 'start', - firstFactor: 'firstFactor', - secondFactor: 'secondFactor', - resetPassword: 'resetPassword', -} as const; - -export type SignInRouterSystemId = keyof typeof SignInRouterSystemId; - -// ---------------------------------- Events ---------------------------------- // - -export type SignInRouterFormAttachEvent = BaseRouterFormAttachEvent; -export type SignInRouterNextEvent = BaseRouterNextEvent; -export type SignInRouterStartEvent = BaseRouterStartEvent; -export type SignInRouterPrevEvent = BaseRouterPrevEvent; -export type SignInRouterChooseStrategyEvent = { type: 'NAVIGATE.CHOOSE_STRATEGY' }; -export type SignInRouterForgotPasswordEvent = { type: 'NAVIGATE.FORGOT_PASSWORD' }; -export type SignInRouterErrorEvent = BaseRouterErrorEvent; -export type SignInRouterTransferEvent = BaseRouterTransferEvent; -export type SignInRouterRedirectEvent = BaseRouterRedirectEvent; -export type SignInRouterResetEvent = BaseRouterResetEvent; -export type SignInRouterResetStepEvent = BaseRouterResetStepEvent; -export type SignInRouterLoadingEvent = BaseRouterLoadingEvent< - 'start' | 'verifications' | 'reset-password' | 'forgot-password' | 'choose-strategy' ->; -export type SignInRouterSetClerkEvent = BaseRouterSetClerkEvent; -export type SignInRouterSubmitEvent = { type: 'SUBMIT' }; -export type SignInRouterPasskeyEvent = { type: 'AUTHENTICATE.PASSKEY' }; -export type SignInRouterPasskeyAutofillEvent = { - type: 'AUTHENTICATE.PASSKEY.AUTOFILL'; -}; -export type SignInRouterSessionSetActiveEvent = { type: 'SESSION.SET_ACTIVE'; id: string }; - -export interface SignInRouterInitEvent extends BaseRouterInput { - type: 'INIT'; - formRef: ActorRefFrom; - signUpPath?: string; -} - -export type SignInRouterNavigationEvents = - | SignInRouterStartEvent - | SignInRouterChooseStrategyEvent - | SignInRouterForgotPasswordEvent - | SignInRouterPrevEvent; - -export type SignInRouterEvents = - | SignInRouterFormAttachEvent - | SignInRouterInitEvent - | SignInRouterNextEvent - | SignInRouterNavigationEvents - | SignInRouterErrorEvent - | SignInRouterTransferEvent - | SignInRouterRedirectEvent - | SignInRouterResetEvent - | SignInRouterResetStepEvent - | SignInVerificationFactorUpdateEvent - | SignInRouterLoadingEvent - | SignInRouterSessionSetActiveEvent - | SignInRouterSetClerkEvent - | SignInRouterSubmitEvent - | SignInRouterPasskeyEvent - | SignInRouterPasskeyAutofillEvent; - -// ---------------------------------- Context ---------------------------------- // - -export type SignInRouterLoadingContext = Omit; - -export interface SignInRouterContext extends BaseRouterContext { - formRef: ActorRefFrom; - loading: SignInRouterLoadingContext; - signUpPath: string; - webAuthnAutofillSupport: boolean; - ticket: string | undefined; -} - -// ---------------------------------- Input ---------------------------------- // - -export type SignInRouterInput = object; - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignInRouterSchema { - context: SignInRouterContext; - events: SignInRouterEvents; - tags: SignInRouterTags; -} - -// ---------------------------------- Schema ---------------------------------- // - -export type SignInRouterChildren = any; // TODO: Update -export type SignInRouterOuptut = any; // TODO: Update -export type SignInRouterStateValue = any; // TODO: Update - -export type SignInRouterSnapshot = MachineSnapshot< - SignInRouterContext, - SignInRouterEvents, - SignInRouterChildren, - SignInRouterStateValue, - SignInRouterTags, - SignInRouterOuptut, - any, // TMeta - Introduced in XState 5.12.x - any // TConfig - Required in newer XState versions ->; - -// ---------------------------------- Machine Type ---------------------------------- // - -export type TSignInRouterParentMachine = StateMachine< - SignInRouterContext, // context - SignInRouterEvents, // event - SignInRouterChildren, // children - any, // actor - any, // action - any, // guard - any, // delay - any, // state value - string, // tag - any, // input - SignInRouterOuptut, // output - any, // emitted - any, // meta - any // config ->; - -// ---------------------------------- Machine Actor Ref ---------------------------------- // - -export type SignInRouterMachineActorRef = ActorRefFrom; diff --git a/packages/elements/src/internals/machines/sign-in/start.machine.ts b/packages/elements/src/internals/machines/sign-in/start.machine.ts deleted file mode 100644 index 8a9f3a9e657..00000000000 --- a/packages/elements/src/internals/machines/sign-in/start.machine.ts +++ /dev/null @@ -1,261 +0,0 @@ -import type { SignInResource, Web3Strategy } from '@clerk/types'; -import { assertEvent, enqueueActions, fromPromise, not, sendTo, setup } from 'xstate'; - -import { SIGN_IN_DEFAULT_BASE_PATH } from '~/internals/constants'; -import { ClerkElementsRuntimeError } from '~/internals/errors'; -import type { FormFields } from '~/internals/machines/form'; -import { sendToLoading } from '~/internals/machines/shared'; -import { assertActorEventError } from '~/internals/machines/utils/assert'; - -import type { SignInRouterMachineActorRef } from './router.types'; -import type { SignInStartSchema } from './start.types'; - -const DISABLEABLE_FIELDS = ['emailAddress', 'phoneNumber'] as const; - -export type TSignInStartMachine = typeof SignInStartMachine; - -export const SignInStartMachineId = 'SignInStart'; - -type AttemptParams = { strategy: 'ticket'; ticket: string } | { strategy?: never; ticket?: never }; - -export const SignInStartMachine = setup({ - actors: { - attemptPasskey: fromPromise< - SignInResource, - { parent: SignInRouterMachineActorRef; flow: 'autofill' | 'discoverable' | undefined } - >(({ input: { parent, flow } }) => { - return parent.getSnapshot().context.clerk.client.signIn.authenticateWithPasskey({ - flow, - }); - }), - attemptWeb3: fromPromise( - ({ input: { parent, strategy } }) => { - if (strategy === 'web3_metamask_signature') { - return parent.getSnapshot().context.clerk.client.signIn.authenticateWithMetamask(); - } - if (strategy === 'web3_coinbase_wallet_signature') { - return parent.getSnapshot().context.clerk.client.signIn.authenticateWithCoinbaseWallet(); - } - if (strategy === 'web3_base_signature') { - return parent.getSnapshot().context.clerk.client.signIn.authenticateWithBase(); - } - if (strategy === 'web3_okx_wallet_signature') { - return parent.getSnapshot().context.clerk.client.signIn.authenticateWithOKXWallet(); - } - throw new ClerkElementsRuntimeError(`Unsupported Web3 strategy: ${strategy}`); - }, - ), - attempt: fromPromise< - SignInResource, - { parent: SignInRouterMachineActorRef; fields: FormFields; params?: AttemptParams } - >(({ input: { fields, parent, params } }) => { - const clerk = parent.getSnapshot().context.clerk; - - const password = fields.get('password'); - const identifier = fields.get('identifier'); - - const passwordParams = password?.value - ? { - password: password.value, - strategy: 'password', - } - : {}; - - return clerk.client.signIn.create({ - ...passwordParams, - ...(params?.ticket - ? params - : { - identifier: (identifier?.value as string) ?? '', - }), - }); - }), - }, - actions: { - sendToNext: ({ context, event }) => { - // @ts-expect-error -- We're calling this in onDone, and event.output exists on the actor done event - return context.parent.send({ type: 'NEXT', resource: event?.output }); - }, - sendToLoading, - setFormDisabledTicketFields: enqueueActions(({ context, enqueue }) => { - if (!context.ticket) { - return; - } - - const currentFields = context.formRef.getSnapshot().context.fields; - - for (const name of DISABLEABLE_FIELDS) { - if (currentFields.has(name)) { - enqueue.sendTo(context.formRef, { type: 'FIELD.DISABLE', field: { name } }); - } - } - }), - setFormErrors: sendTo( - ({ context }) => context.formRef, - ({ event }) => { - assertActorEventError(event); - return { - type: 'ERRORS.SET', - error: event.error, - }; - }, - ), - }, - guards: { - hasTicket: ({ context }) => Boolean(context.ticket), - isExampleMode: ({ context }) => Boolean(context.parent.getSnapshot().context.exampleMode), - }, - types: {} as SignInStartSchema, -}).createMachine({ - id: SignInStartMachineId, - context: ({ input }) => ({ - basePath: input.basePath || SIGN_IN_DEFAULT_BASE_PATH, - parent: input.parent, - formRef: input.formRef, - loadingStep: 'start', - ticket: input.ticket, - }), - initial: 'Init', - states: { - Init: { - description: 'Handle ticket, if present; Else, default to Pending state.', - always: [ - { - guard: 'hasTicket', - target: 'Attempting', - }, - { - target: 'Pending', - }, - ], - }, - Pending: { - tags: ['state:pending'], - description: 'Waiting for user input', - on: { - SUBMIT: { - guard: not('isExampleMode'), - target: 'Attempting', - reenter: true, - }, - 'AUTHENTICATE.PASSKEY': { - guard: not('isExampleMode'), - target: 'AttemptingPasskey', - reenter: true, - }, - 'AUTHENTICATE.PASSKEY.AUTOFILL': { - guard: not('isExampleMode'), - target: 'AttemptingPasskeyAutoFill', - reenter: false, - }, - 'AUTHENTICATE.WEB3': { - guard: not('isExampleMode'), - target: 'AttemptingWeb3', - reenter: true, - }, - }, - }, - Attempting: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attempt', - src: 'attempt', - input: ({ context }) => { - // Standard fields - const defaultParams = { - fields: context.formRef.getSnapshot().context.fields, - parent: context.parent, - }; - - // Handle ticket-specific flows - const params: AttemptParams = context.ticket - ? { - strategy: 'ticket', - ticket: context.ticket, - } - : {}; - - return { ...defaultParams, params }; - }, - onDone: { - actions: ['setFormDisabledTicketFields', 'sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormDisabledTicketFields', 'setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - AttemptingPasskey: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attemptPasskey', - src: 'attemptPasskey', - input: ({ context }) => ({ - parent: context.parent, - flow: 'discoverable', - }), - onDone: { - actions: ['sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - AttemptingPasskeyAutoFill: { - on: { - 'AUTHENTICATE.PASSKEY': { - guard: not('isExampleMode'), - target: 'AttemptingPasskey', - reenter: true, - }, - SUBMIT: { - guard: not('isExampleMode'), - target: 'Attempting', - reenter: true, - }, - }, - invoke: { - id: 'attemptPasskeyAutofill', - src: 'attemptPasskey', - input: ({ context }) => ({ - parent: context.parent, - flow: 'autofill', - }), - onDone: { - actions: ['sendToNext'], - }, - onError: { - actions: ['setFormErrors'], - target: 'Pending', - }, - }, - }, - AttemptingWeb3: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attemptWeb3', - src: 'attemptWeb3', - input: ({ context, event }) => { - assertEvent(event, 'AUTHENTICATE.WEB3'); - return { - parent: context.parent, - strategy: event.strategy, - }; - }, - onDone: { - actions: ['sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - }, -}); diff --git a/packages/elements/src/internals/machines/sign-in/start.types.ts b/packages/elements/src/internals/machines/sign-in/start.types.ts deleted file mode 100644 index e15b9145357..00000000000 --- a/packages/elements/src/internals/machines/sign-in/start.types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { ClerkAPIResponseError } from '@clerk/shared/error'; -import type { Web3Strategy } from '@clerk/types'; -import type { ActorRefFrom, DoneActorEvent, ErrorActorEvent } from 'xstate'; - -import type { FormMachine } from '~/internals/machines/form'; - -import type { SignInRouterMachineActorRef } from './router.types'; - -// ---------------------------------- Tags ---------------------------------- // - -export type SignInStartTags = 'state:pending' | 'state:attempting' | 'state:loading'; - -// ---------------------------------- Events ---------------------------------- // - -export type SignInStartSubmitEvent = { type: 'SUBMIT'; action: 'submit' }; -export type SignInStartPasskeyEvent = { type: 'AUTHENTICATE.PASSKEY'; action: 'passkey' }; -export type SignInStartPasskeyAutofillEvent = { type: 'AUTHENTICATE.PASSKEY.AUTOFILL' }; -export type SignInStartWeb3Event = { type: 'AUTHENTICATE.WEB3'; strategy: Web3Strategy }; - -export type SignInStartEvents = - | ErrorActorEvent - | SignInStartSubmitEvent - | SignInStartPasskeyEvent - | SignInStartPasskeyAutofillEvent - | SignInStartWeb3Event - | DoneActorEvent; - -// ---------------------------------- Input ---------------------------------- // - -export type SignInStartInput = { - basePath?: string; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - ticket?: string | undefined; -}; - -// ---------------------------------- Context ---------------------------------- // - -export interface SignInStartContext { - basePath: string; - error?: Error | ClerkAPIResponseError; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - loadingStep: 'start'; - ticket?: string | undefined; -} - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignInStartSchema { - context: SignInStartContext; - input: SignInStartInput; - events: SignInStartEvents; - tags: SignInStartTags; -} diff --git a/packages/elements/src/internals/machines/sign-in/utils/index.ts b/packages/elements/src/internals/machines/sign-in/utils/index.ts deleted file mode 100644 index 2065da71a47..00000000000 --- a/packages/elements/src/internals/machines/sign-in/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { determineStartingSignInFactor, determineStartingSignInSecondFactor } from './starting-factors'; diff --git a/packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts b/packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts deleted file mode 100644 index 1035720cef7..00000000000 --- a/packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts +++ /dev/null @@ -1,130 +0,0 @@ -// These utilities are ported from: packages/clerk-js/src/ui/components/SignIn/utils.ts -// They should be functionally identical. -import { isWebAuthnSupported } from '@clerk/shared/webauthn'; -import type { - PreferredSignInStrategy, - SignInFactor, - SignInFirstFactor, - SignInSecondFactor, - SignInStrategy, -} from '@clerk/types'; - -// Factor sorting - https://github.com/clerk/javascript/blob/5764e2911790051589bb5c4f3b1a2c79f7f30c7e/packages/clerk-js/src/ui/utils/factorSorting.ts -const makeSortingOrderMap = (arr: T[]): Record => - arr.reduce( - (acc, k, i) => { - acc[k] = i; - return acc; - }, - {} as Record, - ); - -const STRATEGY_SORT_ORDER_PASSWORD_PREF = makeSortingOrderMap([ - 'passkey', - 'password', - 'email_link', - 'email_code', - 'phone_code', -] as SignInStrategy[]); - -const STRATEGY_SORT_ORDER_OTP_PREF = makeSortingOrderMap([ - 'email_code', - 'email_link', - 'phone_code', - 'passkey', - 'password', -] as SignInStrategy[]); - -const makeSortingFunction = - (sortingMap: Record) => - (a: SignInFactor, b: SignInFactor): number => { - const orderA = sortingMap[a.strategy]; - const orderB = sortingMap[b.strategy]; - if (orderA === undefined || orderB === undefined) { - return 0; - } - return orderA - orderB; - }; - -const passwordPrefFactorComparator = makeSortingFunction(STRATEGY_SORT_ORDER_PASSWORD_PREF); -const otpPrefFactorComparator = makeSortingFunction(STRATEGY_SORT_ORDER_OTP_PREF); - -const findFactorForIdentifier = (i: string | null) => (f: SignInFactor) => { - return 'safeIdentifier' in f && f.safeIdentifier === i; -}; - -// The algorithm can be found at -// https://www.notion.so/clerkdev/Implement-sign-in-alt-methods-e6e60ffb644645b3a0553b50556468ce -export function determineStartingSignInFactor( - firstFactors: SignInFirstFactor[] | null, - identifier: string | null, - preferredSignInStrategy?: PreferredSignInStrategy, -) { - if (!firstFactors || firstFactors.length === 0) { - return null; - } - - return preferredSignInStrategy === 'password' - ? determineStrategyWhenPasswordIsPreferred(firstFactors, identifier) - : determineStrategyWhenOTPIsPreferred(firstFactors, identifier); -} - -function findPasskeyStrategy(factors: SignInFirstFactor[]) { - if (isWebAuthnSupported()) { - const passkeyFactor = factors.find(({ strategy }) => strategy === 'passkey'); - - if (passkeyFactor) { - return passkeyFactor; - } - } - return null; -} - -function determineStrategyWhenPasswordIsPreferred(factors: SignInFirstFactor[], identifier: string | null) { - const passkeyFactor = findPasskeyStrategy(factors); - if (passkeyFactor) { - return passkeyFactor; - } - const selected = factors.sort(passwordPrefFactorComparator)[0]; - if (selected.strategy === 'password') { - return selected; - } - return factors.find(findFactorForIdentifier(identifier)) || selected || null; -} - -function determineStrategyWhenOTPIsPreferred(factors: SignInFirstFactor[], identifier: string | null) { - const passkeyFactor = findPasskeyStrategy(factors); - if (passkeyFactor) { - return passkeyFactor; - } - - const sortedBasedOnPrefFactor = factors.sort(otpPrefFactorComparator); - const forIdentifier = sortedBasedOnPrefFactor.find(findFactorForIdentifier(identifier)); - if (forIdentifier) { - return forIdentifier; - } - const firstBasedOnPref = sortedBasedOnPrefFactor[0]; - if (firstBasedOnPref.strategy === 'email_link') { - return firstBasedOnPref; - } - return factors.find(findFactorForIdentifier(identifier)) || firstBasedOnPref || null; -} - -// The priority of second factors is: TOTP -> Phone code -> any other factor -export function determineStartingSignInSecondFactor(secondFactors: SignInSecondFactor[] | null) { - if (!secondFactors || secondFactors.length === 0) { - return null; - } - - const totpFactor = secondFactors.find(f => f.strategy === 'totp'); - if (totpFactor) { - return totpFactor; - } - - const phoneCodeFactor = secondFactors.find(f => f.strategy === 'phone_code'); - if (phoneCodeFactor) { - return phoneCodeFactor; - } - - return secondFactors[0]; -} diff --git a/packages/elements/src/internals/machines/sign-in/verification.machine.ts b/packages/elements/src/internals/machines/sign-in/verification.machine.ts deleted file mode 100644 index 208adc3b190..00000000000 --- a/packages/elements/src/internals/machines/sign-in/verification.machine.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { isClerkAPIResponseError } from '@clerk/shared/error'; -import type { - AttemptFirstFactorParams, - EmailCodeAttempt, - PasswordAttempt, - PhoneCodeAttempt, - PrepareFirstFactorParams, - PrepareSecondFactorParams, - ResetPasswordEmailCodeAttempt, - ResetPasswordPhoneCodeAttempt, - SignInFirstFactor, - SignInResource, - SignInSecondFactor, - Web3Attempt, -} from '@clerk/types'; -import type { DoneActorEvent } from 'xstate'; -import { assign, fromPromise, log, sendTo, setup } from 'xstate'; - -import { - MAGIC_LINK_VERIFY_PATH_ROUTE, - RESENDABLE_COUNTDOWN_DEFAULT, - SIGN_IN_DEFAULT_BASE_PATH, -} from '~/internals/constants'; -import { ClerkElementsRuntimeError } from '~/internals/errors'; -import type { FormFields } from '~/internals/machines/form'; -import type { SignInStrategyName, WithParams } from '~/internals/machines/shared'; -import { sendToLoading } from '~/internals/machines/shared'; -import { determineStartingSignInFactor, determineStartingSignInSecondFactor } from '~/internals/machines/sign-in/utils'; -import { assertActorEventError, assertIsDefined } from '~/internals/machines/utils/assert'; - -import type { SignInRouterMachineActorRef } from './router.types'; -import type { SignInVerificationSchema } from './verification.types'; -import { SignInVerificationDelays } from './verification.types'; - -export type TSignInFirstFactorMachine = typeof SignInFirstFactorMachine; -export type TSignInSecondFactorMachine = typeof SignInSecondFactorMachine; - -export type DetermineStartingFactorInput = { - parent: SignInRouterMachineActorRef; -}; - -export type PrepareFirstFactorInput = WithParams & { - parent: SignInRouterMachineActorRef; - resendable: boolean; -}; -export type PrepareSecondFactorInput = WithParams & { - parent: SignInRouterMachineActorRef; - resendable: boolean; -}; - -export type AttemptFirstFactorInput = { - parent: SignInRouterMachineActorRef; - fields: FormFields; - currentFactor: SignInFirstFactor | null; -}; -export type AttemptSecondFactorInput = { - parent: SignInRouterMachineActorRef; - fields: FormFields; - currentFactor: SignInSecondFactor | null; -}; - -const isNonPreparableStrategy = (strategy?: SignInFirstFactor['strategy'] | SignInSecondFactor['strategy']) => { - if (!strategy) { - return false; - } - - return ['passkey', 'password'].includes(strategy); -}; - -export const SignInVerificationMachineId = 'SignInVerification'; - -const SignInVerificationMachine = setup({ - actors: { - determineStartingFactor: fromPromise( - () => Promise.reject(new ClerkElementsRuntimeError('Actor `determineStartingFactor` must be overridden')), - ), - prepare: fromPromise(() => - Promise.reject(new ClerkElementsRuntimeError('Actor `prepare` must be overridden')), - ), - attempt: fromPromise(() => - Promise.reject(new ClerkElementsRuntimeError('Actor `attempt` must be overridden')), - ), - attemptPasskey: fromPromise< - SignInResource, - { parent: SignInRouterMachineActorRef; flow: 'autofill' | 'discoverable' | undefined } - >(({ input: { parent, flow } }) => { - return parent.getSnapshot().context.clerk.client.signIn.authenticateWithPasskey({ - flow, - }); - }), - }, - actions: { - resendableTick: assign(({ context }) => ({ - resendable: context.resendableAfter === 0, - resendableAfter: context.resendableAfter > 0 ? context.resendableAfter - 1 : context.resendableAfter, - })), - resendableReset: assign({ - resendable: false, - resendableAfter: RESENDABLE_COUNTDOWN_DEFAULT, - }), - validateRegisteredStrategies: ({ context }) => { - const clerk = context.parent.getSnapshot().context.clerk; - - if (clerk.__unstable__environment?.isProduction()) { - return; - } - - // Only show these warnings in development! - if (process.env.NODE_ENV === 'development') { - if ( - clerk.client.signIn.supportedFirstFactors && - !clerk.client.signIn.supportedFirstFactors.every((factor: SignInFirstFactor) => - context.registeredStrategies.has(factor.strategy), - ) - ) { - console.warn( - `Clerk: Your instance is configured to support these strategies: ${clerk.client.signIn.supportedFirstFactors - .map((factor: SignInFirstFactor) => factor.strategy) - .join(', ')}, but the rendered strategies are: ${Array.from(context.registeredStrategies).join( - ', ', - )}. Make sure to render a component for each supported strategy. More information: https://clerk.com/docs/elements/reference/sign-in#strategy`, - ); - } - - if ( - clerk.client.signIn.supportedSecondFactors && - !clerk.client.signIn.supportedSecondFactors.every((factor: SignInSecondFactor) => - context.registeredStrategies.has(factor.strategy), - ) - ) { - console.warn( - `Clerk: Your instance is configured to support these 2FA strategies: ${clerk.client.signIn.supportedSecondFactors - .map((f: SignInSecondFactor) => f.strategy) - .join(', ')}, but the rendered strategies are: ${Array.from(context.registeredStrategies).join( - ', ', - )}. Make sure to render a component for each supported strategy. More information: https://clerk.com/docs/elements/reference/sign-in#strategy`, - ); - } - - const strategiesUsedButNotActivated = Array.from(context.registeredStrategies).filter( - strategy => - !clerk.client.signIn.supportedFirstFactors?.some( - (supported: SignInFirstFactor) => supported.strategy === strategy, - ), - ); - - if (strategiesUsedButNotActivated.length > 0) { - console.warn( - `Clerk: These rendered strategies are not configured for your instance: ${strategiesUsedButNotActivated.join(', ')}. If this is unexpected, make sure to enable them in your Clerk dashboard: https://dashboard.clerk.com/last-active?path=/user-authentication/email-phone-username`, - ); - } - - if (context.currentFactor?.strategy && !context.registeredStrategies.has(context.currentFactor?.strategy)) { - throw new ClerkElementsRuntimeError( - `Your sign-in attempt is missing a ${context.currentFactor?.strategy} strategy. Make sure is rendered in your flow. More information: https://clerk.com/docs/elements/reference/sign-in#strategy`, - ); - } else if (!context.currentFactor?.strategy) { - throw new ClerkElementsRuntimeError( - 'Unable to determine an authentication strategy to verify. This means your instance is misconfigured. Visit the Clerk Dashboard and verify that your instance has authentication strategies enabled: https://dashboard.clerk.com/last-active?path=/user-authentication/email-phone-username', - ); - } - } - }, - sendToNext: ({ context, event }) => - context.parent.send({ type: 'NEXT', resource: (event as unknown as DoneActorEvent).output }), - sendToLoading, - setFormErrors: sendTo( - ({ context }) => context.formRef, - ({ event }) => { - assertActorEventError(event); - return { - type: 'ERRORS.SET', - error: event.error, - }; - }, - ), - setConsoleError: ({ event }) => { - if (process.env.NODE_ENV !== 'development') { - return; - } - - assertActorEventError(event); - - const error = isClerkAPIResponseError(event.error) ? event.error.errors[0].longMessage : event.error.message; - - console.error(`Unable to fulfill the prepare or attempt request for the sign-in verification. - Error: ${error} - Please open an issue if you continue to run into this issue.`); - }, - }, - guards: { - isResendable: ({ context }) => context.resendable || context.resendableAfter === 0, - isNeverResendable: ({ context }) => isNonPreparableStrategy(context.currentFactor?.strategy), - }, - delays: SignInVerificationDelays, - types: {} as SignInVerificationSchema, -}).createMachine({ - id: SignInVerificationMachineId, - context: ({ input }) => ({ - currentFactor: null, - basePath: input.basePath || SIGN_IN_DEFAULT_BASE_PATH, - formRef: input.formRef, - loadingStep: 'verifications', - parent: input.parent, - registeredStrategies: new Set(), - resendable: false, - resendableAfter: RESENDABLE_COUNTDOWN_DEFAULT, - }), - initial: 'Init', - on: { - 'NAVIGATE.PREVIOUS': '.Hist', - 'STRATEGY.REGISTER': { - actions: assign({ - registeredStrategies: ({ context, event }) => context.registeredStrategies.add(event.factor), - }), - }, - 'STRATEGY.UNREGISTER': { - actions: assign({ - registeredStrategies: ({ context, event }) => { - context.registeredStrategies.delete(event.factor); - return context.registeredStrategies; - }, - }), - }, - }, - states: { - Init: { - tags: ['state:preparing', 'state:loading'], - invoke: { - id: 'determineStartingFactor', - src: 'determineStartingFactor', - input: ({ context }) => ({ - parent: context.parent, - }), - onDone: { - target: 'Preparing', - actions: assign({ - currentFactor: ({ event }) => event.output, - }), - }, - onError: { - target: 'Preparing', - actions: [ - log('Clerk [Sign In Verification]: Error determining starting factor'), - assign({ - currentFactor: { strategy: 'password' }, - }), - ], - }, - }, - }, - Preparing: { - tags: ['state:preparing', 'state:loading'], - invoke: { - id: 'prepare', - src: 'prepare', - input: ({ context }) => ({ - parent: context.parent, - resendable: context.resendable, - params: { - ...context.currentFactor, - redirectUrl: `${window.location.origin}${context.basePath}${MAGIC_LINK_VERIFY_PATH_ROUTE}`, - } as PrepareFirstFactorParams, - }), - onDone: { - actions: 'resendableReset', - target: 'Pending', - }, - onError: { - actions: ['setFormErrors', 'setConsoleError'], - target: 'Pending', - }, - }, - }, - Pending: { - tags: ['state:pending'], - description: 'Waiting for user input', - on: { - 'AUTHENTICATE.PASSKEY': { - target: 'AttemptingPasskey', - reenter: true, - }, - 'NAVIGATE.CHOOSE_STRATEGY': 'ChooseStrategy', - 'NAVIGATE.FORGOT_PASSWORD': 'ChooseStrategy', - RETRY: 'Preparing', - SUBMIT: { - target: 'Attempting', - reenter: true, - }, - }, - initial: 'Init', - states: { - Init: { - description: 'Marks appropriate factors as never resendable.', - always: [ - { - guard: 'isNeverResendable', - target: 'NeverResendable', - }, - { - target: 'NotResendable', - }, - ], - }, - Resendable: { - description: 'Waiting for user to retry', - }, - NeverResendable: { - description: 'Handles never resendable', - on: { - RETRY: { - actions: log('Never retriable'), - }, - }, - }, - NotResendable: { - description: 'Handle countdowns', - on: { - RETRY: { - actions: log(({ context }) => `Not retriable; Try again in ${context.resendableAfter}s`), - }, - }, - after: { - resendableTimeout: [ - { - description: 'Set as retriable if countdown is 0', - guard: 'isResendable', - actions: 'resendableTick', - target: 'Resendable', - }, - { - description: 'Continue countdown if not retriable', - actions: 'resendableTick', - target: 'NotResendable', - reenter: true, - }, - ], - }, - }, - }, - after: { - 3000: { - actions: 'validateRegisteredStrategies', - }, - }, - }, - ChooseStrategy: { - description: 'Handles both choose strategy and forgot password as the latter is similar in functionality', - tags: ['state:choose-strategy', 'state:forgot-password'], - on: { - 'STRATEGY.UPDATE': { - actions: assign({ currentFactor: ({ event }) => event.factor || null }), - target: 'Preparing', - }, - }, - }, - Attempting: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attempt', - src: 'attempt', - input: ({ context }) => ({ - parent: context.parent, - currentFactor: context.currentFactor as SignInFirstFactor | null, - fields: context.formRef.getSnapshot().context.fields, - }), - onDone: { - actions: ['sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'setConsoleError', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - AttemptingPasskey: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attemptPasskey', - src: 'attemptPasskey', - input: ({ context }) => ({ - parent: context.parent, - flow: 'discoverable', - }), - onDone: { - actions: ['sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - Hist: { - type: 'history', - }, - }, -}); - -export const SignInFirstFactorMachine = SignInVerificationMachine.provide({ - actors: { - determineStartingFactor: fromPromise(async ({ input }) => { - const clerk = input.parent.getSnapshot().context.clerk; - - return Promise.resolve( - determineStartingSignInFactor( - clerk.client.signIn.supportedFirstFactors, - clerk.client.signIn.identifier, - clerk.__unstable__environment?.displayConfig.preferredSignInStrategy, - ), - ); - }), - prepare: fromPromise(async ({ input }) => { - // `input` is a union of PrepareFirstFactor and PrepareSecondFactor. Since we're passing params to - // prepareFirstFactor, we need to assert that the input is a PrepareFirstFactor. For some reason, ESLint thinks - // the assertion is unnecessary, and will remove it during the pre-commit hook. To prevent that, we disable the - // rule for the line. - - const { params, parent, resendable } = input as PrepareFirstFactorInput; - const clerk = parent.getSnapshot().context.clerk; - - // If a prepare call has already been fired recently, don't re-send - const currentVerificationExpiration = clerk.client.signIn.firstFactorVerification.expireAt; - const needsPrepare = resendable || !currentVerificationExpiration || currentVerificationExpiration < new Date(); - - if (isNonPreparableStrategy(params?.strategy) || !needsPrepare) { - return Promise.resolve(clerk.client.signIn); - } - - assertIsDefined(params, 'First factor params'); - return await clerk.client.signIn.prepareFirstFactor(params); - }), - attempt: fromPromise(async ({ input }) => { - const { currentFactor, fields, parent } = input as AttemptFirstFactorInput; - - assertIsDefined(currentFactor, 'Current factor'); - - let attemptParams: AttemptFirstFactorParams; - - const strategy = currentFactor.strategy; - const code = fields.get('code')?.value as string | undefined; - const password = fields.get('password')?.value as string | undefined; - - switch (strategy) { - case 'passkey': { - return await parent.getSnapshot().context.clerk.client.signIn.authenticateWithPasskey(); - } - case 'password': { - assertIsDefined(password, 'Password'); - - attemptParams = { - strategy, - password, - } satisfies PasswordAttempt; - - break; - } - case 'reset_password_phone_code': - case 'reset_password_email_code': { - assertIsDefined(code, 'Code for resetting phone/email'); - - attemptParams = { - strategy, - code, - password, - } satisfies ResetPasswordPhoneCodeAttempt | ResetPasswordEmailCodeAttempt; - - break; - } - case 'phone_code': - case 'email_code': { - assertIsDefined(code, 'Code for phone/email'); - - attemptParams = { - strategy, - code, - } satisfies PhoneCodeAttempt | EmailCodeAttempt; - - break; - } - case 'web3_metamask_signature': { - const signature = fields.get('signature')?.value as string | undefined; - assertIsDefined(signature, 'Web3 Metamask signature'); - - attemptParams = { - strategy, - signature, - } satisfies Web3Attempt; - - break; - } - case 'web3_coinbase_wallet_signature': { - const signature = fields.get('signature')?.value as string | undefined; - assertIsDefined(signature, 'Web3 Coinbase Wallet signature'); - - attemptParams = { - strategy, - signature, - } satisfies Web3Attempt; - - break; - } - case 'web3_okx_wallet_signature': { - const signature = fields.get('signature')?.value as string | undefined; - assertIsDefined(signature, 'Web3 OKX Wallet signature'); - - attemptParams = { - strategy, - signature, - } satisfies Web3Attempt; - - break; - } - default: - throw new ClerkElementsRuntimeError(`Invalid strategy: ${strategy}`); - } - - return await parent.getSnapshot().context.clerk.client.signIn.attemptFirstFactor(attemptParams); - }), - }, -}); - -export const SignInSecondFactorMachine = SignInVerificationMachine.provide({ - actors: { - determineStartingFactor: fromPromise(async ({ input }) => - Promise.resolve( - determineStartingSignInSecondFactor( - input.parent.getSnapshot().context.clerk.client.signIn.supportedSecondFactors, - ), - ), - ), - prepare: fromPromise(async ({ input }) => { - const { params, parent, resendable } = input as PrepareSecondFactorInput; - const clerk = parent.getSnapshot().context.clerk; - - // If a prepare call has already been fired recently, don't re-send - const currentVerificationExpiration = clerk.client.signIn.secondFactorVerification.expireAt; - const needsPrepare = resendable || !currentVerificationExpiration || currentVerificationExpiration < new Date(); - - assertIsDefined(params, 'Second factor params'); - - if (params.strategy !== 'phone_code' || !needsPrepare) { - return Promise.resolve(clerk.client.signIn); - } - - return await clerk.client.signIn.prepareSecondFactor({ - strategy: params.strategy, - phoneNumberId: params.phoneNumberId, - }); - }), - attempt: fromPromise(async ({ input }) => { - const { fields, parent, currentFactor } = input as AttemptSecondFactorInput; - - const code = fields.get('code')?.value as string; - - assertIsDefined(currentFactor, 'Current factor'); - assertIsDefined(code, 'Code'); - - return await parent.getSnapshot().context.clerk.client.signIn.attemptSecondFactor({ - // @ts-expect-error - email_link is not supported in the attemptSecondFactor params - strategy: currentFactor.strategy, - code, - }); - }), - }, -}); diff --git a/packages/elements/src/internals/machines/sign-in/verification.types.ts b/packages/elements/src/internals/machines/sign-in/verification.types.ts deleted file mode 100644 index 4c0c32dd1cc..00000000000 --- a/packages/elements/src/internals/machines/sign-in/verification.types.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { ClerkAPIResponseError } from '@clerk/shared/error'; -import type { SignInFactor } from '@clerk/types'; -import type { ActorRefFrom, ErrorActorEvent } from 'xstate'; - -import type { FormMachine } from '~/internals/machines/form'; -import type { SignInStrategyName } from '~/internals/machines/shared'; - -import type { SignInRouterMachineActorRef } from './router.types'; - -// ---------------------------------- Tags ---------------------------------- // - -export type SignInVerificationTags = - | 'state:attempting' - | 'state:choose-strategy' - | 'state:forgot-password' - | 'state:loading' - | 'state:pending' - | 'state:preparing'; - -// ---------------------------------- Events ---------------------------------- // - -export type SignInVerificationSubmitEvent = { type: 'SUBMIT'; action: 'submit' }; -export type SignInVerificationFactorUpdateEvent = { type: 'STRATEGY.UPDATE'; factor: SignInFactor | undefined }; -export type SignInVerificationRetryEvent = { type: 'RETRY' }; -export type SignInVerificationStrategyRegisterEvent = { type: 'STRATEGY.REGISTER'; factor: SignInStrategyName }; -export type SignInVerificationStrategyUnregisterEvent = { type: 'STRATEGY.UNREGISTER'; factor: SignInStrategyName }; - -export type SignInVerificationEvents = - | ErrorActorEvent - | SignInVerificationSubmitEvent - | SignInVerificationFactorUpdateEvent - | SignInVerificationRetryEvent - | SignInVerificationStrategyRegisterEvent - | SignInVerificationStrategyUnregisterEvent; - -// ---------------------------------- Input ---------------------------------- // - -export interface SignInVerificationInput { - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - basePath?: string; -} - -// ---------------------------------- Context ---------------------------------- // - -export interface SignInVerificationContext { - currentFactor: SignInFactor | null; - error?: Error | ClerkAPIResponseError; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - loadingStep: 'verifications'; - registeredStrategies: Set; - resendable: boolean; - resendableAfter: number; - basePath?: string; -} - -// ---------------------------------- Delays ---------------------------------- // - -export const SignInVerificationDelays = { - resendableTimeout: 1_000, // 1 second -} as const; - -export type SignInVerificationDelays = keyof typeof SignInVerificationDelays; - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignInVerificationSchema { - context: SignInVerificationContext; - input: SignInVerificationInput; - delays: SignInVerificationDelays; - events: SignInVerificationEvents; - tags: SignInVerificationTags; -} diff --git a/packages/elements/src/internals/machines/sign-up/continue.machine.ts b/packages/elements/src/internals/machines/sign-up/continue.machine.ts deleted file mode 100644 index dee44be5f61..00000000000 --- a/packages/elements/src/internals/machines/sign-up/continue.machine.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { snakeToCamel } from '@clerk/shared/underscore'; -import type { SignUpResource } from '@clerk/types'; -import type { DoneActorEvent } from 'xstate'; -import { fromPromise, not, or, setup } from 'xstate'; - -import { SIGN_UP_DEFAULT_BASE_PATH } from '~/internals/constants'; -import type { FormDefaultValues, FormFields } from '~/internals/machines/form'; -import { sendToLoading } from '~/internals/machines/shared'; -import { fieldsToSignUpParams } from '~/internals/machines/sign-up/utils'; -import { assertActorEventError } from '~/internals/machines/utils/assert'; - -import type { SignUpContinueSchema } from './continue.types'; -import type { SignInRouterMachineActorRef } from './router.types'; - -export type TSignUpContinueMachine = typeof SignUpContinueMachine; - -export const SignUpContinueMachineId = 'SignUpContinue'; - -export const SignUpContinueMachine = setup({ - actors: { - attempt: fromPromise( - ({ input: { fields, parent } }) => { - const params = fieldsToSignUpParams(fields); - return parent.getSnapshot().context.clerk.client.signUp.update(params); - }, - ), - }, - actions: { - setFormErrors: ({ context, event }) => { - assertActorEventError(event); - context.formRef.send({ - type: 'ERRORS.SET', - error: event.error, - }); - }, - markFormAsProgressive: ({ context }) => { - const signUp = context.parent.getSnapshot().context.clerk.client.signUp; - - const missing = signUp.missingFields.map(snakeToCamel); - const optional = signUp.optionalFields.map(snakeToCamel); - const required = signUp.requiredFields.map(snakeToCamel); - - const progressiveFieldValues: FormDefaultValues = new Map(); - - for (const key of required.concat(optional) as (keyof SignUpResource)[]) { - if (key in signUp) { - progressiveFieldValues.set(key, signUp[key] as string | number | readonly string[] | undefined); - } - } - - context.formRef.send({ - type: 'MARK_AS_PROGRESSIVE', - missing, - optional, - required, - defaultValues: progressiveFieldValues, - }); - }, - unmarkFormAsProgressive: ({ context }) => context.formRef.send({ type: 'UNMARK_AS_PROGRESSIVE' }), - sendToNext: ({ context, event }) => - context.parent.send({ type: 'NEXT', resource: (event as unknown as DoneActorEvent).output }), - sendToLoading, - }, - guards: { - isStatusMissingRequirements: ({ context }) => - context.parent.getSnapshot().context.clerk?.client?.signUp?.status === 'missing_requirements', - hasMetPreviousMissingRequirements: ({ context }) => { - const signUp = context.parent.getSnapshot().context.clerk.client.signUp; - - const fields = context.formRef.getSnapshot().context.fields; - const signUpMissingFields = signUp.missingFields.map(snakeToCamel); - const missingFields = Array.from(context.formRef.getSnapshot().context.fields.keys()).filter(key => { - return !signUpMissingFields.includes(key) && !fields.get(key)?.value && !fields.get(key)?.checked; - }); - - return missingFields.length === 0; - }, - }, - types: {} as SignUpContinueSchema, -}).createMachine({ - id: SignUpContinueMachineId, - context: ({ input }) => ({ - basePath: input.basePath || SIGN_UP_DEFAULT_BASE_PATH, - formRef: input.formRef, - parent: input.parent, - loadingStep: 'continue', - }), - entry: 'markFormAsProgressive', - onDone: { - actions: 'unmarkFormAsProgressive', - }, - initial: 'Pending', - states: { - Pending: { - tags: ['state:pending'], - description: 'Waiting for user input', - on: { - SUBMIT: { - guard: or(['hasMetPreviousMissingRequirements', not('isStatusMissingRequirements')]), - target: 'Attempting', - reenter: true, - }, - }, - }, - Attempting: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attempt', - src: 'attempt', - input: ({ context }) => ({ - parent: context.parent, - fields: context.formRef.getSnapshot().context.fields, - }), - onDone: { - actions: ['sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - }, -}); diff --git a/packages/elements/src/internals/machines/sign-up/continue.types.ts b/packages/elements/src/internals/machines/sign-up/continue.types.ts deleted file mode 100644 index 52b1276e692..00000000000 --- a/packages/elements/src/internals/machines/sign-up/continue.types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ClerkAPIResponseError } from '@clerk/shared/error'; -import type { ActorRefFrom, ErrorActorEvent } from 'xstate'; - -import type { FormMachine } from '~/internals/machines/form'; - -import type { SignInRouterMachineActorRef } from './router.types'; - -// ---------------------------------- Tags ---------------------------------- // - -export type SignUpContinueTags = 'state:pending' | 'state:attempting' | 'state:loading'; - -// ---------------------------------- Events ---------------------------------- // - -export type SignUpContinueSubmitEvent = { type: 'SUBMIT'; action: 'submit' }; - -export type SignUpContinueEvents = ErrorActorEvent | SignUpContinueSubmitEvent; - -// ---------------------------------- Input ---------------------------------- // - -export type SignUpContinueInput = { - basePath?: string; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; -}; - -// ---------------------------------- Context ---------------------------------- // - -export interface SignUpContinueContext { - basePath: string; - error?: Error | ClerkAPIResponseError; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - loadingStep: 'continue'; -} - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignUpContinueSchema { - context: SignUpContinueContext; - input: SignUpContinueInput; - events: SignUpContinueEvents; - tags: SignUpContinueTags; -} diff --git a/packages/elements/src/internals/machines/sign-up/index.ts b/packages/elements/src/internals/machines/sign-up/index.ts deleted file mode 100644 index be94c5b3404..00000000000 --- a/packages/elements/src/internals/machines/sign-up/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export { SignUpContinueMachine, SignUpContinueMachineId } from './continue.machine'; -export { SignUpRouterMachine, SignUpRouterMachineId } from './router.machine'; -export { SignUpStartMachine, SignUpStartMachineId } from './start.machine'; -export { SignUpVerificationMachine, SignUpVerificationMachineId } from './verification.machine'; - -export type { TSignUpContinueMachine } from './continue.machine'; -export type { TSignUpRouterMachine } from './router.machine'; -export type { TSignUpStartMachine } from './start.machine'; -export type { TSignUpVerificationMachine } from './verification.machine'; - -export type * from './continue.types'; -export type * from './router.types'; -export type * from './start.types'; -export type * from './verification.types'; diff --git a/packages/elements/src/internals/machines/sign-up/router.machine.ts b/packages/elements/src/internals/machines/sign-up/router.machine.ts deleted file mode 100644 index f3dcf09b8cc..00000000000 --- a/packages/elements/src/internals/machines/sign-up/router.machine.ts +++ /dev/null @@ -1,542 +0,0 @@ -import { joinURL } from '@clerk/shared/url'; -import type { SignUpStatus, VerificationStatus } from '@clerk/types'; -import type { NonReducibleUnknown } from 'xstate'; -import { and, assign, enqueueActions, log, not, or, raise, sendTo, setup } from 'xstate'; - -import { - ERROR_CODES, - ROUTING, - SEARCH_PARAMS, - SIGN_IN_DEFAULT_BASE_PATH, - SIGN_UP_DEFAULT_BASE_PATH, - SIGN_UP_MODES, - SSO_CALLBACK_PATH_ROUTE, -} from '~/internals/constants'; -import { ClerkElementsError, ClerkElementsRuntimeError } from '~/internals/errors'; -import { ThirdPartyMachine, ThirdPartyMachineId } from '~/internals/machines/third-party'; -import { shouldUseVirtualRouting } from '~/internals/machines/utils/next'; - -import { SignUpContinueMachine } from './continue.machine'; -import type { - SignUpRouterContext, - SignUpRouterEvents, - SignUpRouterNextEvent, - SignUpRouterSchema, -} from './router.types'; -import { SignUpStartMachine } from './start.machine'; -import { SignUpVerificationMachine } from './verification.machine'; - -export const SignUpRouterMachineId = 'SignUpRouter'; -export type TSignUpRouterMachine = typeof SignUpRouterMachine; - -const isCurrentPath = - (path: `/${string}`) => - ({ context }: { context: SignUpRouterContext }, _params?: NonReducibleUnknown) => - context.router?.match(path) ?? false; - -const needsStatus = - (status: SignUpStatus) => - ({ context, event }: { context: SignUpRouterContext; event?: SignUpRouterEvents }, _?: NonReducibleUnknown) => - (event as SignUpRouterNextEvent)?.resource?.status === status || context.clerk?.client?.signUp?.status === status; - -export const SignUpRouterMachine = setup({ - actors: { - continueMachine: SignUpContinueMachine, - startMachine: SignUpStartMachine, - thirdPartyMachine: ThirdPartyMachine, - verificationMachine: SignUpVerificationMachine, - }, - actions: { - clearFormErrors: sendTo(({ context }) => context.formRef, { type: 'ERRORS.CLEAR' }), - logUnknownError: snapshot => console.error('Unknown error:', snapshot), - navigateInternal: ({ context }, { path, force = false }: { path: string; force?: boolean }) => { - if (!context.router) { - return; - } - if (!force && shouldUseVirtualRouting()) { - return; - } - if (context.exampleMode) { - return; - } - - const resolvedPath = joinURL(context.router.basePath, path); - if (resolvedPath === context.router.pathname()) { - return; - } - - context.router.shallowPush(resolvedPath); - }, - navigateExternal: ({ context }, { path }: { path: string }) => context.router?.push(path), - raiseNext: raise({ type: 'NEXT' }), - setActive: ({ context, event }, params?: { sessionId?: string; useLastActiveSession?: boolean }) => { - if (context.exampleMode) { - return; - } - - const session = - params?.sessionId || - (params?.useLastActiveSession && context.clerk.client.lastActiveSessionId) || - ((event as SignUpRouterNextEvent)?.resource || context.clerk.client.signUp).createdSessionId; - - void context.clerk.setActive({ - session, - redirectUrl: context.clerk.buildAfterSignUpUrl({ - params: context.router?.searchParams(), - }), - }); - }, - delayedReset: raise({ type: 'RESET' }, { delay: 3000 }), // Reset machine after 3s delay. - setError: assign({ - error: (_, { error }: { error?: ClerkElementsError }) => { - if (error) { - return error; - } - return new ClerkElementsRuntimeError('Unknown error'); - }, - }), - setFormOAuthErrors: ({ context }) => { - const errorOrig = context.clerk.client.signIn.firstFactorVerification.error; - - if (!errorOrig) { - return; - } - - let error: ClerkElementsError; - - switch (errorOrig.code) { - case ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP: - case ERROR_CODES.OAUTH_ACCESS_DENIED: - case ERROR_CODES.NOT_ALLOWED_ACCESS: - case ERROR_CODES.SAML_USER_ATTRIBUTE_MISSING: - case ERROR_CODES.OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: - case ERROR_CODES.USER_LOCKED: - case ERROR_CODES.ENTERPRISE_SSO_USER_ATTRIBUTE_MISSING: - case ERROR_CODES.ENTERPRISE_SSO_EMAIL_ADDRESS_DOMAIN_MISMATCH: - case ERROR_CODES.ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: - case ERROR_CODES.SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: - case ERROR_CODES.ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - error = new ClerkElementsError(errorOrig.code, errorOrig.longMessage!); - break; - default: - error = new ClerkElementsError( - 'unable_to_complete', - 'Unable to complete action at this time. If the problem persists please contact support.', - ); - } - - context.formRef.send({ - type: 'ERRORS.SET', - error, - }); - }, - transfer: ({ context }) => context.router?.push(context.clerk.buildSignInUrl()), - }, - guards: { - areFieldsMissing: ({ context }) => context.clerk?.client?.signUp?.missingFields?.length > 0, - areFieldsUnverified: ({ context }) => context.clerk?.client?.signUp?.unverifiedFields?.length > 0, - - hasAuthenticatedViaClerkJS: ({ context }) => - Boolean(context.clerk.client.signUp.status === null && context.clerk.client.lastActiveSessionId), - hasCreatedSession: ({ context }) => Boolean(context.router?.searchParams().get(SEARCH_PARAMS.createdSession)), - hasClerkStatus: ({ context }, params?: { status: VerificationStatus }) => { - const value = context.router?.searchParams().get(SEARCH_PARAMS.status); - if (!params) { - return Boolean(value); - } - return value === params.status; - }, - hasClerkTransfer: ({ context }) => Boolean(context.router?.searchParams().get(SEARCH_PARAMS.transfer)), - hasResource: ({ context }) => Boolean(context.clerk.client.signUp), - hasTicket: ({ context }) => Boolean(context.ticket), - - isLoggedInAndSingleSession: and(['isLoggedIn', 'isSingleSessionMode', not('isExampleMode')]), - isStatusAbandoned: needsStatus('abandoned'), - isStatusComplete: ({ context, event }) => { - const resource = (event as SignUpRouterNextEvent)?.resource; - const signUp = context.clerk?.client?.signUp; - - return ( - (resource?.status === 'complete' && Boolean(resource?.createdSessionId)) || - (signUp?.status === 'complete' && Boolean(signUp?.createdSessionId)) - ); - }, - isStatusMissingRequirements: needsStatus('missing_requirements'), - - isLoggedIn: or(['isStatusComplete', ({ context }) => Boolean(context.clerk.user)]), - isSingleSessionMode: ({ context }) => Boolean(context.clerk?.__unstable__environment?.authConfig.singleSessionMode), - isRestricted: ({ context }) => - context.clerk?.__unstable__environment?.userSettings.signUp.mode === SIGN_UP_MODES.RESTRICTED, - isRestrictedWithoutTicket: and(['isRestricted', not('hasTicket')]), - isExampleMode: ({ context }) => Boolean(context.exampleMode), - isMissingRequiredFields: and(['isStatusMissingRequirements', 'areFieldsMissing']), - isMissingRequiredUnverifiedFields: and(['isStatusMissingRequirements', 'areFieldsUnverified']), - - needsIdentifier: or(['statusNeedsIdentifier', isCurrentPath('/')]), - needsContinue: and(['statusNeedsContinue', isCurrentPath('/continue')]), - needsVerification: and(['statusNeedsVerification', isCurrentPath('/verify')]), - needsCallback: isCurrentPath(SSO_CALLBACK_PATH_ROUTE), - - statusNeedsIdentifier: or([not('hasResource'), 'isStatusAbandoned']), - statusNeedsContinue: or(['isMissingRequiredFields']), - statusNeedsVerification: or(['isMissingRequiredUnverifiedFields', and(['areFieldsMissing', 'hasClerkStatus'])]), - }, - delays: { - 'TIMEOUT.POLLING': 300_000, // 5 minutes - }, - types: {} as SignUpRouterSchema, -}).createMachine({ - id: SignUpRouterMachineId, - // @ts-expect-error - Set in INIT event - context: {}, - initial: 'Idle', - on: { - 'AUTHENTICATE.OAUTH': { - actions: sendTo(ThirdPartyMachineId, ({ context, event }) => ({ - type: 'REDIRECT', - params: { - strategy: event.strategy, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signUpUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, - redirectUrlComplete: context.clerk.buildAfterSignUpUrl({ - params: context.router?.searchParams(), - }), - }, - })), - }, - 'AUTHENTICATE.SAML': { - actions: sendTo(ThirdPartyMachineId, ({ context }) => ({ - type: 'REDIRECT', - params: { - strategy: 'saml', - emailAddress: context.formRef.getSnapshot().context.fields.get('emailAddress')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signUpUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, - redirectUrlComplete: context.clerk.buildAfterSignUpUrl({ - params: context.router?.searchParams(), - }), - }, - })), - }, - 'AUTHENTICATE.ENTERPRISE_SSO': { - actions: sendTo(ThirdPartyMachineId, ({ context }) => ({ - type: 'REDIRECT', - params: { - strategy: 'enterprise_sso', - emailAddress: context.formRef.getSnapshot().context.fields.get('emailAddress')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signUpUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, - redirectUrlComplete: context.clerk.buildAfterSignUpUrl({ - params: context.router?.searchParams(), - }), - }, - })), - }, - 'AUTHENTICATE.WEB3': { - actions: sendTo('start', ({ event }) => event), - }, - 'FORM.ATTACH': { - description: 'Attach/re-attach the form to the router.', - actions: enqueueActions(({ enqueue, event }) => { - enqueue.assign({ - formRef: event.formRef, - }); - - // Reset the current step, to reset the form reference. - enqueue.raise({ type: 'RESET.STEP' }); - }), - }, - 'NAVIGATE.PREVIOUS': '.Hist', - 'NAVIGATE.START': '.Start', - LOADING: { - actions: assign(({ event }) => ({ - loading: { - isLoading: event.isLoading, - step: event.step, - strategy: event.strategy, - action: event.action, - }, - })), - }, - RESET: '.Idle', - }, - states: { - Idle: { - on: { - INIT: { - actions: assign(({ event }) => { - const searchParams = event.router?.searchParams(); - - return { - clerk: event.clerk, - router: event.router, - signInPath: event.signInPath || SIGN_IN_DEFAULT_BASE_PATH, - loading: { - isLoading: false, - }, - exampleMode: event.exampleMode || false, - formRef: event.formRef, - ticket: - searchParams?.get(SEARCH_PARAMS.ticket) || - searchParams?.get(SEARCH_PARAMS.invitationToken) || - undefined, - }; - }), - target: 'Init', - }, - }, - }, - Init: { - entry: enqueueActions(({ context, enqueue, self }) => { - if (!self.getSnapshot().children[ThirdPartyMachineId]) { - enqueue.spawnChild('thirdPartyMachine', { - id: ThirdPartyMachineId, - systemId: ThirdPartyMachineId, - input: { - basePath: context.router?.basePath ?? SIGN_UP_DEFAULT_BASE_PATH, - flow: 'signUp', - formRef: context.formRef, - parent: self, - }, - }); - } - }), - always: [ - { - guard: 'isLoggedInAndSingleSession', - actions: [ - log('Already logged in'), - { - type: 'navigateExternal', - params: ({ context }) => ({ - path: context.clerk.buildAfterSignUpUrl({ - params: context.router?.searchParams(), - }), - }), - }, - ], - }, - { - guard: 'needsCallback', - target: 'Callback', - }, - { - guard: 'hasTicket', - actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, - target: 'Start', - }, - { - guard: 'needsVerification', - actions: { type: 'navigateInternal', params: { force: true, path: '/verify' } }, - target: 'Verification', - }, - { - guard: or(['needsContinue', 'hasClerkTransfer']), - actions: { type: 'navigateInternal', params: { force: true, path: '/continue' } }, - target: 'Continue', - }, - { - guard: 'isRestrictedWithoutTicket', - target: 'Restricted', - }, - { - actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, - target: 'Start', - }, - ], - }, - Start: { - tags: ['step:start'], - exit: 'clearFormErrors', - invoke: { - id: 'start', - src: 'startMachine', - input: ({ context, self }) => ({ - basePath: context.router?.basePath, - formRef: context.formRef, - parent: self, - ticket: context.ticket, - }), - onDone: { - actions: 'raiseNext', - }, - }, - on: { - 'RESET.STEP': { - actions: enqueueActions(({ enqueue, context }) => { - enqueue('clearFormErrors'); - enqueue.sendTo('start', { type: 'SET_FORM', formRef: context.formRef }); - }), - }, - NEXT: [ - { - guard: 'isStatusComplete', - actions: ['setActive', 'delayedReset'], - }, - { - guard: and(['hasTicket', 'statusNeedsContinue']), - actions: { type: 'navigateInternal', params: { path: '/' } }, - target: 'Start', - reenter: true, - }, - { - guard: 'statusNeedsVerification', - target: 'Verification', - actions: { type: 'navigateInternal', params: { path: '/verify' } }, - }, - { - guard: 'statusNeedsContinue', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'Continue', - }, - ], - }, - }, - Continue: { - tags: ['step:continue'], - invoke: { - id: 'continue', - src: 'continueMachine', - input: ({ context, self }) => ({ - basePath: context.router?.basePath, - formRef: context.formRef, - parent: self, - }), - onDone: { - actions: 'raiseNext', - }, - }, - on: { - 'RESET.STEP': { - target: 'Continue', - reenter: true, - }, - NEXT: [ - { - guard: 'isStatusComplete', - actions: ['setActive', 'delayedReset'], - }, - { - guard: 'statusNeedsVerification', - target: 'Verification', - actions: { type: 'navigateInternal', params: { path: '/verify' } }, - }, - ], - }, - }, - Verification: { - tags: ['step:verification'], - invoke: { - id: 'verification', - src: 'verificationMachine', - input: ({ context, self }) => ({ - attributes: context.clerk.__unstable__environment?.userSettings.attributes, - basePath: context.router?.basePath, - formRef: context.formRef, - parent: self, - resource: context.clerk.client.signUp, - }), - onDone: { - actions: 'raiseNext', - }, - }, - always: [ - { - guard: 'hasCreatedSession', - actions: [ - ({ context }) => ({ - type: 'setActive', - params: { sessionId: context.router?.searchParams().get(SEARCH_PARAMS.createdSession) }, - }), - 'delayedReset', - ], - }, - { - guard: { type: 'hasClerkStatus', params: { status: 'verified' } }, - actions: { type: 'navigateInternal', params: { force: true, path: '/continue' } }, - }, - { - guard: { type: 'hasClerkStatus', params: { status: 'expired' } }, - actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, - }, - ], - on: { - 'RESET.STEP': { - target: 'Verification', - reenter: true, - }, - NEXT: [ - { - guard: 'isStatusComplete', - actions: ['setActive', 'delayedReset'], - }, - { - guard: 'statusNeedsContinue', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'Continue', - }, - ], - }, - }, - Callback: { - tags: ['step:callback'], - entry: sendTo(ThirdPartyMachineId, { type: 'CALLBACK' }), - on: { - NEXT: [ - { - guard: 'isStatusComplete', - actions: ['setActive', 'delayedReset'], - }, - { - description: 'Handle a case where the user has already been authenticated via ClerkJS', - guard: 'hasAuthenticatedViaClerkJS', - actions: [{ type: 'setActive', params: { useLastActiveSession: true } }, 'delayedReset'], - }, - { - guard: 'statusNeedsVerification', - actions: { type: 'navigateInternal', params: { path: '/verify' } }, - target: 'Verification', - }, - { - guard: 'statusNeedsContinue', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, - target: 'Continue', - }, - { - actions: { type: 'navigateInternal', params: { path: '/' } }, - target: 'Start', - }, - ], - }, - }, - Restricted: { - tags: ['step:restricted'], - on: { - NEXT: 'Start', - }, - }, - Error: { - tags: ['step:error'], - on: { - NEXT: { - target: 'Start', - actions: 'clearFormErrors', - }, - }, - }, - Hist: { - type: 'history', - exit: 'clearFormErrors', - }, - }, -}); diff --git a/packages/elements/src/internals/machines/sign-up/router.types.ts b/packages/elements/src/internals/machines/sign-up/router.types.ts deleted file mode 100644 index 217ef7265d9..00000000000 --- a/packages/elements/src/internals/machines/sign-up/router.types.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { SignUpResource } from '@clerk/types'; -import type { ActorRefFrom, SnapshotFrom, StateMachine } from 'xstate'; - -import type { TFormMachine } from '~/internals/machines/form'; -import type { - BaseRouterContext, - BaseRouterErrorEvent, - BaseRouterFormAttachEvent, - BaseRouterInput, - BaseRouterLoadingEvent, - BaseRouterNextEvent, - BaseRouterPrevEvent, - BaseRouterRedirectEvent, - BaseRouterResetEvent, - BaseRouterResetStepEvent, - BaseRouterSetClerkEvent, - BaseRouterStartEvent, - BaseRouterTransferEvent, -} from '~/internals/machines/types'; - -// ---------------------------------- Tags ---------------------------------- // - -export const SignUpRouterSteps = { - start: 'step:start', - continue: 'step:continue', - verification: 'step:verification', - callback: 'step:callback', - error: 'step:error', - restricted: 'step:restricted', -} as const; - -export type SignUpRouterSteps = keyof typeof SignUpRouterSteps; -export type SignUpRouterTags = (typeof SignUpRouterSteps)[keyof typeof SignUpRouterSteps]; - -// ---------------------------------- Children ---------------------------------- // - -export const SignUpRouterSystemId = { - start: 'start', - continue: 'continue', - verification: 'verification', -} as const; - -export type SignUpRouterSystemId = keyof typeof SignUpRouterSystemId; - -// ---------------------------------- Events ---------------------------------- // - -export type SignUpRouterFormAttachEvent = BaseRouterFormAttachEvent; -export type SignUpRouterNextEvent = BaseRouterNextEvent; -export type SignUpRouterStartEvent = BaseRouterStartEvent; -export type SignUpRouterPrevEvent = BaseRouterPrevEvent; -export type SignUpRouterErrorEvent = BaseRouterErrorEvent; -export type SignUpRouterTransferEvent = BaseRouterTransferEvent; -export type SignUpRouterRedirectEvent = BaseRouterRedirectEvent; -export type SignUpRouterResetEvent = BaseRouterResetEvent; -export type SignUpRouterResetStepEvent = BaseRouterResetStepEvent; -export type SignUpRouterLoadingEvent = BaseRouterLoadingEvent<'start' | 'verifications' | 'continue'>; -export type SignUpRouterSetClerkEvent = BaseRouterSetClerkEvent; - -export interface SignUpRouterInitEvent extends BaseRouterInput { - type: 'INIT'; - formRef: ActorRefFrom; - signInPath?: string; -} - -export type SignUpRouterNavigationEvents = SignUpRouterStartEvent | SignUpRouterPrevEvent; - -export type SignUpRouterEvents = - | SignUpRouterFormAttachEvent - | SignUpRouterInitEvent - | SignUpRouterNextEvent - | SignUpRouterNavigationEvents - | SignUpRouterErrorEvent - | SignUpRouterTransferEvent - | SignUpRouterRedirectEvent - | SignUpRouterResetEvent - | SignUpRouterResetStepEvent - | SignUpRouterLoadingEvent - | SignUpRouterSetClerkEvent; - -// ---------------------------------- Delays ---------------------------------- // - -export const SignUpRouterDelays = { - polling: 300_000, // 5 minutes -} as const; - -export type SignUpRouterDelays = keyof typeof SignUpRouterDelays; - -// ---------------------------------- Context ---------------------------------- // - -export type SignUpRouterLoadingContext = Omit; - -export interface SignUpRouterContext extends BaseRouterContext { - formRef: ActorRefFrom; - loading: SignUpRouterLoadingContext; - signInPath: string; - ticket: string | undefined; -} - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignUpRouterSchema { - context: SignUpRouterContext; - events: SignUpRouterEvents; - tags: SignUpRouterTags; - delays: SignUpRouterDelays; -} - -// ---------------------------------- Machine Type ---------------------------------- // - -export type SignUpRouterChildren = any; // TODO: Update -export type SignUpRouterOuptut = any; // TODO: Update -export type SignUpRouterStateValue = any; // TODO: Update - -export type TSignUpRouterParentMachine = StateMachine< - SignUpRouterContext, // context - SignUpRouterEvents, // event - SignUpRouterChildren, // children - any, // actor - any, // action - any, // guard - any, // delay - SignUpRouterStateValue, // state value - string, // tag - any, // input - SignUpRouterOuptut, // output - any, // emitted - any, // meta - Introduced in XState 5.12.x - any // config - Required in newer XState versions ->; - -// ---------------------------------- Machine Actor Ref ---------------------------------- // - -export type SignInRouterMachineActorRef = ActorRefFrom; - -// ---------------------------------- Snapshot ---------------------------------- // - -export type SignUpRouterSnapshot = SnapshotFrom; diff --git a/packages/elements/src/internals/machines/sign-up/start.machine.ts b/packages/elements/src/internals/machines/sign-up/start.machine.ts deleted file mode 100644 index 315207acc1d..00000000000 --- a/packages/elements/src/internals/machines/sign-up/start.machine.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { SignUpResource, Web3Strategy } from '@clerk/types'; -import type { DoneActorEvent } from 'xstate'; -import { and, assertEvent, assign, enqueueActions, fromPromise, not, sendTo, setup } from 'xstate'; - -import { SIGN_UP_DEFAULT_BASE_PATH } from '~/internals/constants'; -import { ClerkElementsRuntimeError } from '~/internals/errors'; -import type { FormFields } from '~/internals/machines/form'; -import type { SetFormEvent } from '~/internals/machines/shared'; -import { sendToLoading } from '~/internals/machines/shared'; -import { fieldsToSignUpParams } from '~/internals/machines/sign-up/utils'; -import { ThirdPartyMachine } from '~/internals/machines/third-party'; -import { assertActorEventError } from '~/internals/machines/utils/assert'; - -import type { SignInRouterMachineActorRef } from './router.types'; -import type { SignUpStartSchema } from './start.types'; - -const DISABLEABLE_FIELDS = ['emailAddress', 'phoneNumber'] as const; - -export type TSignUpStartMachine = typeof SignUpStartMachine; - -export const SignUpStartMachineId = 'SignUpStart'; - -type AttemptParams = { strategy: 'ticket'; ticket: string } | { strategy?: never; ticket?: never }; -type PrefillFieldsKeys = keyof Pick< - SignUpResource, - 'username' | 'firstName' | 'lastName' | 'emailAddress' | 'phoneNumber' ->; -const PREFILL_FIELDS: PrefillFieldsKeys[] = ['firstName', 'lastName', 'emailAddress', 'username', 'phoneNumber']; - -export const SignUpStartMachine = setup({ - actors: { - attempt: fromPromise< - SignUpResource, - { parent: SignInRouterMachineActorRef; fields: FormFields; params?: AttemptParams } - >(({ input: { fields, parent, params } }) => { - const fieldParams = fieldsToSignUpParams(fields); - return parent.getSnapshot().context.clerk.client.signUp.create({ ...fieldParams, ...params }); - }), - attemptWeb3: fromPromise( - ({ input: { parent, strategy } }) => { - if (strategy === 'web3_metamask_signature') { - return parent.getSnapshot().context.clerk.client.signUp.authenticateWithMetamask(); - } - if (strategy === 'web3_coinbase_wallet_signature') { - return parent.getSnapshot().context.clerk.client.signUp.authenticateWithCoinbaseWallet(); - } - if (strategy === 'web3_okx_wallet_signature') { - return parent.getSnapshot().context.clerk.client.signUp.authenticateWithOKXWallet(); - } - throw new ClerkElementsRuntimeError(`Unsupported Web3 strategy: ${strategy}`); - }, - ), - thirdParty: ThirdPartyMachine, - }, - actions: { - sendToNext: ({ context, event }) => - context.parent.send({ type: 'NEXT', resource: (event as unknown as DoneActorEvent).output }), - sendToLoading, - setFormRef: assign(({ event }) => { - return { - formRef: (event as unknown as SetFormEvent).formRef, - }; - }), - setFormDisabledTicketFields: enqueueActions(({ context, enqueue }) => { - if (!context.ticket) { - return; - } - - const currentFields = context.formRef.getSnapshot().context.fields; - - for (const name of DISABLEABLE_FIELDS) { - if (currentFields.has(name)) { - enqueue.sendTo(context.formRef, { type: 'FIELD.DISABLE', field: { name } }); - } - } - }), - setFormErrors: sendTo( - ({ context }) => context.formRef, - ({ event }) => { - assertActorEventError(event); - return { - type: 'ERRORS.SET', - error: event.error, - }; - }, - ), - setDefaultFormValues: ({ context }) => { - const signUp = context.parent.getSnapshot().context.clerk.client.signUp; - const prefilledDefaultValues = new Map(); - - for (const key of PREFILL_FIELDS) { - if (key in signUp) { - prefilledDefaultValues.set(key, signUp[key]); - } - } - - context.formRef.send({ - type: 'PREFILL_DEFAULT_VALUES', - defaultValues: prefilledDefaultValues, - }); - }, - }, - guards: { - isMissingRequirements: ({ context }) => - context.parent.getSnapshot().context.clerk?.client?.signUp?.status === 'missing_requirements', - hasTicket: ({ context }) => Boolean(context.ticket), - isExampleMode: ({ context }) => Boolean(context.parent.getSnapshot().context.exampleMode), - }, - types: {} as SignUpStartSchema, -}).createMachine({ - id: SignUpStartMachineId, - context: ({ input }) => ({ - basePath: input.basePath || SIGN_UP_DEFAULT_BASE_PATH, - formRef: input.formRef, - parent: input.parent, - loadingStep: 'start', - ticket: input.ticket, - }), - entry: 'setDefaultFormValues', - initial: 'Init', - on: { - SET_FORM: { - actions: 'setFormRef', - }, - }, - states: { - Init: { - description: - 'Handle ticket, if present; Else, default to Pending state. Per tickets, `Attempting` makes a `signUp.create` request allowing for an incomplete sign up to contain progressively filled fields on the Start step.', - always: [ - { - guard: and(['hasTicket', 'isMissingRequirements']), - target: 'Pending', - }, - { - guard: 'hasTicket', - target: 'Attempting', - }, - { - target: 'Pending', - }, - ], - }, - Pending: { - tags: ['state:pending'], - description: 'Waiting for user input', - on: { - SUBMIT: { - guard: not('isExampleMode'), - target: 'Attempting', - reenter: true, - }, - 'AUTHENTICATE.WEB3': { - guard: not('isExampleMode'), - target: 'AttemptingWeb3', - reenter: true, - }, - }, - }, - Attempting: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attemptCreate', - src: 'attempt', - input: ({ context }) => { - // Standard fields - const defaultParams = { - fields: context.formRef.getSnapshot().context.fields, - parent: context.parent, - }; - - // Handle ticket-specific flows - const params: AttemptParams = context.ticket - ? { - strategy: 'ticket', - ticket: context.ticket, - } - : {}; - - return { ...defaultParams, params }; - }, - onDone: { - actions: ['setFormDisabledTicketFields', 'sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormDisabledTicketFields', 'setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - AttemptingWeb3: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attemptCreateWeb3', - src: 'attemptWeb3', - input: ({ context, event }) => { - assertEvent(event, 'AUTHENTICATE.WEB3'); - return { - parent: context.parent, - strategy: event.strategy, - }; - }, - onDone: { - actions: ['sendToNext', 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - }, -}); diff --git a/packages/elements/src/internals/machines/sign-up/start.types.ts b/packages/elements/src/internals/machines/sign-up/start.types.ts deleted file mode 100644 index edbaec7b389..00000000000 --- a/packages/elements/src/internals/machines/sign-up/start.types.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { ClerkAPIResponseError } from '@clerk/shared/error'; -import type { EnterpriseSSOStrategy, OAuthStrategy, SamlStrategy, Web3Strategy } from '@clerk/types'; -import type { ActorRefFrom, ErrorActorEvent } from 'xstate'; - -import type { FormMachine } from '~/internals/machines/form'; - -import type { SetFormEvent } from '../shared'; -import type { SignInRouterMachineActorRef } from './router.types'; - -// ---------------------------------- Tags ---------------------------------- // - -export type SignUpStartTags = 'state:pending' | 'state:attempting' | 'state:loading'; - -// ---------------------------------- Events ---------------------------------- // - -export type SignUpStartSubmitEvent = { type: 'SUBMIT'; action: 'submit' }; - -// TODO: Consolidate with SignInStartMachine -export type SignUpStartRedirectOauthEvent = { type: 'AUTHENTICATE.OAUTH'; strategy: OAuthStrategy }; -export type SignUpStartRedirectSamlEvent = { type: 'AUTHENTICATE.SAML'; strategy?: SamlStrategy }; -export type SignUpStartRedirectEnterpriseSSOEvent = { - type: 'AUTHENTICATE.ENTERPRISE_SSO'; - strategy?: EnterpriseSSOStrategy; -}; -export type SignUpStartRedirectWeb3Event = { type: 'AUTHENTICATE.WEB3'; strategy: Web3Strategy }; - -export type SignUpStartRedirectEvent = - | SignUpStartRedirectOauthEvent - | SignUpStartRedirectSamlEvent - | SignUpStartRedirectWeb3Event - | SignUpStartRedirectEnterpriseSSOEvent; - -export type SignUpStartEvents = ErrorActorEvent | SignUpStartSubmitEvent | SignUpStartRedirectEvent | SetFormEvent; - -// ---------------------------------- Input ---------------------------------- // - -export type SignUpStartInput = { - basePath?: string; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - ticket?: string | undefined; -}; - -// ---------------------------------- Context ---------------------------------- // - -export interface SignUpStartContext { - basePath: string; - error?: Error | ClerkAPIResponseError; - loadingStep: 'start'; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - ticket?: string | undefined; -} - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignUpStartSchema { - context: SignUpStartContext; - input: SignUpStartInput; - events: SignUpStartEvents; - tags: SignUpStartTags; -} diff --git a/packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts b/packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts deleted file mode 100644 index ffe29242530..00000000000 --- a/packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { fieldsToSignUpParams } from '../fields-to-params'; - -describe('fieldsToSignUpParams', () => { - it('converts form fields to sign up params', () => { - const fields = new Map([ - ['firstName', { type: 'text', value: 'John' }], - ['emailAddress', { type: 'text', value: 'john@example.com' }], - ['password', { type: 'text', value: 'password123' }], - ]); - - const params = fieldsToSignUpParams(fields); - - expect(params).toEqual({ - firstName: 'John', - emailAddress: 'john@example.com', - password: 'password123', - }); - }); - - it('ignores undefined values', () => { - const fields = new Map([ - ['firstName', { type: 'text', value: 'John' }], - ['emailAddress', { type: 'text', value: undefined }], - ['password', { type: 'text', value: 'password123' }], - ]); - - const params = fieldsToSignUpParams(fields); - - expect(params).toEqual({ - firstName: 'John', - password: 'password123', - }); - }); - - it('ignores non-sign-up keys', () => { - const fields = new Map([ - ['firstName', { type: 'text', value: 'John' }], - ['foo', { type: 'text', value: 'bar' }], - ['bar', { type: 'text', value: 'foo' }], - ]); - - const params = fieldsToSignUpParams(fields); - - expect(params).toEqual({ - firstName: 'John', - }); - }); -}); diff --git a/packages/elements/src/internals/machines/sign-up/utils/fields-to-params.ts b/packages/elements/src/internals/machines/sign-up/utils/fields-to-params.ts deleted file mode 100644 index 3ebe006eee4..00000000000 --- a/packages/elements/src/internals/machines/sign-up/utils/fields-to-params.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { SignUpCreateParams, SignUpUpdateParams } from '@clerk/types'; - -import type { FormFields } from '~/internals/machines/form'; - -const SignUpAdditionalKeys = [ - 'firstName', - 'lastName', - 'emailAddress', - 'username', - 'password', - 'phoneNumber', - 'legalAccepted', -] as const; - -type SignUpAdditionalKeys = (typeof SignUpAdditionalKeys)[number]; - -const signUpKeys = new Set(SignUpAdditionalKeys); - -function isSignUpParam(key: string): key is T { - return signUpKeys.has(key as T); -} - -export function fieldsToSignUpParams( - fields: FormFields, -): Pick { - const params: SignUpUpdateParams = {}; - - fields.forEach(({ value, checked, type }, key) => { - if (isSignUpParam(key) && value !== undefined && type !== 'checkbox') { - // @ts-expect-error - Type is not narrowed to string - params[key] = value as string; - } - - if (isSignUpParam(key) && checked !== undefined && type === 'checkbox') { - // @ts-expect-error - Type is not narrowed to boolean - params[key] = checked; - } - }); - - return params; -} diff --git a/packages/elements/src/internals/machines/sign-up/utils/index.ts b/packages/elements/src/internals/machines/sign-up/utils/index.ts deleted file mode 100644 index e9b1197a1be..00000000000 --- a/packages/elements/src/internals/machines/sign-up/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { fieldsToSignUpParams } from './fields-to-params'; diff --git a/packages/elements/src/internals/machines/sign-up/verification.machine.ts b/packages/elements/src/internals/machines/sign-up/verification.machine.ts deleted file mode 100644 index 63e27144200..00000000000 --- a/packages/elements/src/internals/machines/sign-up/verification.machine.ts +++ /dev/null @@ -1,559 +0,0 @@ -import { Poller } from '@clerk/shared/poller'; -import type { - AttemptVerificationParams, - Attribute, - PrepareVerificationParams, - SignUpResource, - SignUpVerifiableField, - SignUpVerificationsResource, - VerificationStrategy, -} from '@clerk/types'; -import type { Writable } from 'type-fest'; -import { and, assign, enqueueActions, fromCallback, fromPromise, log, raise, sendParent, sendTo, setup } from 'xstate'; - -import { - MAGIC_LINK_VERIFY_PATH_ROUTE, - RESENDABLE_COUNTDOWN_DEFAULT, - SIGN_UP_DEFAULT_BASE_PATH, -} from '~/internals/constants'; -import { ClerkElementsError, ClerkElementsRuntimeError } from '~/internals/errors'; -import type { WithParams } from '~/internals/machines/shared'; -import { sendToLoading } from '~/internals/machines/shared'; -import { assertActorEventError } from '~/internals/machines/utils/assert'; - -import type { SignInRouterMachineActorRef } from './router.types'; -import { - type SignUpVerificationContext, - SignUpVerificationDelays, - type SignUpVerificationEmailLinkFailedEvent, - type SignUpVerificationEvents, - type SignUpVerificationSchema, -} from './verification.types'; - -export type SignUpVerificationsResourceKey = keyof SignUpVerificationsResource; -export type TSignUpVerificationMachine = typeof SignUpVerificationMachine; - -export type StartSignUpEmailLinkFlowEvents = { type: 'STOP' }; -export type StartSignUpEmailLinkFlowInput = { - parent: SignInRouterMachineActorRef; -}; - -export const SignUpVerificationMachineId = 'SignUpVerification'; - -const shouldVerify = (field: SignUpVerifiableField, strategy?: VerificationStrategy) => { - const guards: Writable>[0]> = [ - { - type: 'isFieldUnverified', - params: { - field, - }, - }, - ]; - - if (strategy) { - guards.push({ - type: 'isStrategyEnabled', - params: { - attribute: field, - strategy, - }, - }); - } - - return and(guards); -}; - -export type PrepareVerificationInput = { - parent: SignInRouterMachineActorRef; -} & WithParams; -export type AttemptVerificationInput = { - parent: SignInRouterMachineActorRef; -} & WithParams; - -export const SignUpVerificationMachine = setup({ - actors: { - prepare: fromPromise(({ input: { params, parent } }) => { - const clerk = parent.getSnapshot().context.clerk; - - if (params.strategy === 'email_link' && params.redirectUrl) { - params.redirectUrl = clerk.buildUrlWithAuth(params.redirectUrl); - } - - return clerk.client.signUp.prepareVerification(params); - }), - attempt: fromPromise(async ({ input: { params, parent } }) => - parent.getSnapshot().context.clerk.client.signUp.attemptVerification(params), - ), - attemptEmailLinkVerification: fromCallback( - ({ receive, sendBack, input: { parent } }) => { - const { run, stop } = Poller(); - - const clerk = parent.getSnapshot().context.clerk; - - void run(async () => - clerk.client.signUp - .reload() - .then((resource: SignUpResource) => { - const signInStatus = resource.status; - const verificationStatus = resource.verifications.emailAddress.status; - - // Short-circuit if the sign-up resource is already complete - if (signInStatus === 'complete') { - return sendBack({ type: 'EMAIL_LINK.VERIFIED', resource }); - } - - switch (verificationStatus) { - case 'verified': - case 'transferable': - case 'expired': { - sendBack({ type: `EMAIL_LINK.${verificationStatus.toUpperCase()}`, resource }); - break; - } - case 'failed': { - sendBack({ - type: 'EMAIL_LINK.FAILED', - error: new ClerkElementsError('email-link-verification-failed', 'Email verification failed'), - resource, - }); - break; - } - // case 'unverified': - default: - return; - } - - stop(); - }) - .catch((error: Error) => { - stop(); - new ClerkElementsRuntimeError(error.message); - }), - ); - - receive(event => { - if (event.type === 'STOP') { - stop(); - } - }); - - return () => stop(); - }, - ), - }, - actions: { - resendableTick: assign(({ context }) => ({ - resendable: context.resendableAfter === 1, - resendableAfter: context.resendableAfter > 1 ? context.resendableAfter - 1 : context.resendableAfter, - })), - resendableReset: assign({ - resendable: false, - resendableAfter: RESENDABLE_COUNTDOWN_DEFAULT, - }), - sendToLoading, - setFormErrors: sendTo( - ({ context }) => context.formRef, - ({ event }) => { - assertActorEventError(event); - return { - type: 'ERRORS.SET', - error: event.error, - }; - }, - ), - }, - guards: { - isComplete: ({ context }) => context.resource.status === 'complete', - isFieldUnverified: ({ context, event }, { field }: { field: SignUpVerifiableField }) => { - let resource = context.resource; - - if (event?.type === 'NEXT' && event.resource) { - resource = event.resource; - } - - return resource.unverifiedFields.includes(field); - }, - isResendable: ({ context }) => context.resendable || context.resendableAfter === 0, - isStrategyEnabled: ( - { context }, - { attribute, strategy }: { attribute: Attribute; strategy: VerificationStrategy }, - ) => Boolean(context.attributes?.[attribute].verifications.includes(strategy)), - shouldVerifyPhoneCode: shouldVerify('phone_number'), - shouldVerifyEmailLink: shouldVerify('email_address', 'email_link'), - shouldVerifyEmailCode: shouldVerify('email_address', 'email_code'), - }, - delays: SignUpVerificationDelays, - types: {} as SignUpVerificationSchema, -}).createMachine({ - id: SignUpVerificationMachineId, - initial: 'Init', - context: ({ input }) => ({ - attributes: input.attributes, - basePath: input.basePath || SIGN_UP_DEFAULT_BASE_PATH, - loadingStep: 'verifications', - formRef: input.formRef, - parent: input.parent, - resendable: false, - resendableAfter: RESENDABLE_COUNTDOWN_DEFAULT, - resource: input.resource, - }), - on: { - NEXT: [ - { - guard: 'isComplete', - actions: sendParent(({ event }) => ({ type: 'NEXT', resource: event.resource })), - }, - { - description: 'Validate via phone number', - guard: 'shouldVerifyPhoneCode', - target: '.PhoneCode', - }, - { - description: 'Validate via email link', - guard: 'shouldVerifyEmailLink', - target: '.EmailLink', - }, - { - description: 'Verify via email code', - guard: 'shouldVerifyEmailCode', - target: '.EmailCode', - }, - { - actions: sendParent(({ event }) => ({ type: 'NEXT', resource: event.resource })), - }, - ], - }, - states: { - Init: { - always: [ - { - description: 'Validate via phone number', - guard: 'shouldVerifyPhoneCode', - target: 'PhoneCode', - }, - { - description: 'Validate via email link', - guard: 'shouldVerifyEmailLink', - target: 'EmailLink', - }, - { - description: 'Verify via email code', - guard: 'shouldVerifyEmailCode', - target: 'EmailCode', - }, - { - actions: sendParent(({ context }) => ({ type: 'NEXT', resource: context.resource })), - }, - ], - }, - EmailLink: { - tags: ['verification:method:email', 'verification:category:link', 'verification:email_link'], - initial: 'Preparing', - on: { - RETRY: '.Preparing', - 'EMAIL_LINK.RESTART': { - target: '.Attempting', - reenter: true, - }, - 'EMAIL_LINK.FAILED': { - actions: [ - { - type: 'setFormErrors', - params: ({ event }: { event: SignUpVerificationEmailLinkFailedEvent }) => ({ error: event.error }), - }, - assign({ resource: ({ event }) => event.resource }), - ], - target: '.Pending', - }, - 'EMAIL_LINK.*': { - actions: enqueueActions(({ enqueue, event }) => { - if (event.type === 'EMAIL_LINK.RESTART') { - return; - } - - enqueue.assign({ resource: event.resource }); - enqueue.raise({ type: 'NEXT', resource: event.resource }); - }), - }, - }, - states: { - Preparing: { - tags: ['state:preparing', 'state:loading'], - exit: 'resendableReset', - invoke: { - id: 'prepareEmailLinkVerification', - src: 'prepare', - input: ({ context }) => ({ - parent: context.parent, - params: { - strategy: 'email_link', - redirectUrl: `${context.basePath}${MAGIC_LINK_VERIFY_PATH_ROUTE}`, - }, - }), - onDone: { - target: 'Attempting', - actions: assign({ resource: ({ event }) => event.output }), - }, - onError: { - actions: 'setFormErrors', - target: 'Pending', - }, - }, - }, - Pending: { - description: 'Placeholder for allowing resending of email link', - tags: ['state:pending'], - on: { - NEXT: 'Preparing', - }, - }, - Attempting: { - tags: ['state:attempting'], - invoke: { - id: 'attemptEmailLinkVerification', - src: 'attemptEmailLinkVerification', - input: ({ context }) => ({ - parent: context.parent, - }), - }, - after: { - emailLinkTimeout: { - description: 'Timeout after 5 minutes', - target: 'Pending', - actions: sendTo(({ context }) => context.formRef, { - type: 'ERRORS.SET', - error: new ClerkElementsError('verify-email-link-timeout', 'Email link verification timed out'), - }), - }, - }, - initial: 'NotResendable', - states: { - Resendable: { - description: 'Waiting for user to retry', - }, - NotResendable: { - description: 'Handle countdowns', - on: { - RETRY: { - actions: log(({ context }) => `Not retriable; Try again in ${context.resendableAfter}s`), - }, - }, - after: { - resendableTimeout: [ - { - description: 'Set as retriable if countdown is 0', - guard: 'isResendable', - actions: 'resendableTick', - target: 'Resendable', - }, - { - description: 'Continue countdown if not retriable', - actions: 'resendableTick', - target: 'NotResendable', - reenter: true, - }, - ], - }, - }, - }, - }, - }, - }, - EmailCode: { - tags: ['verification:method:email', 'verification:category:code', 'verification:email_code'], - initial: 'Preparing', - states: { - Preparing: { - tags: ['state:preparing', 'state:loading'], - exit: 'resendableReset', - invoke: { - id: 'prepareEmailAddressCodeVerification', - src: 'prepare', - input: ({ context }) => ({ - parent: context.parent, - params: { - strategy: 'email_code', - }, - }), - onDone: [ - { - guard: 'shouldVerifyEmailCode', - target: 'Pending', - }, - { - actions: [ - assign({ resource: ({ event }) => event.output }), - raise(({ event }) => ({ type: 'NEXT', resource: event.output })), - ], - }, - ], - onError: { - actions: 'setFormErrors', - target: 'Pending', - }, - }, - }, - Pending: { - tags: ['state:pending'], - on: { - RETRY: 'Preparing', - SUBMIT: { - target: 'Attempting', - reenter: true, - }, - }, - initial: 'NotResendable', - states: { - Resendable: { - description: 'Waiting for user to retry', - }, - NotResendable: { - description: 'Handle countdowns', - on: { - RETRY: { - actions: log(({ context }) => `Not retriable; Try again in ${context.resendableAfter}s`), - }, - }, - after: { - resendableTimeout: [ - { - description: 'Set as retriable if countdown is 0', - guard: 'isResendable', - actions: 'resendableTick', - target: 'Resendable', - }, - { - description: 'Continue countdown if not retriable', - actions: 'resendableTick', - target: 'NotResendable', - reenter: true, - }, - ], - }, - }, - }, - }, - Attempting: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attemptEmailAddressCodeVerification', - src: 'attempt', - input: ({ context }) => ({ - parent: context.parent, - params: { - strategy: 'email_code', - code: (context.formRef.getSnapshot().context.fields.get('code')?.value as string) || '', - }, - }), - onDone: { - actions: [raise(({ event }) => ({ type: 'NEXT', resource: event.output })), 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - }, - }, - PhoneCode: { - tags: ['verification:method:phone', 'verification:category:code', 'verification:phone_code'], - initial: 'Preparing', - states: { - Preparing: { - tags: ['state:preparing', 'state:loading'], - exit: 'resendableReset', - invoke: { - id: 'preparePhoneCodeVerification', - src: 'prepare', - input: ({ context }) => ({ - parent: context.parent, - params: { - strategy: 'phone_code', - }, - }), - onDone: [ - { - guard: 'shouldVerifyPhoneCode', - target: 'Pending', - actions: assign({ resource: ({ event }) => event.output }), - }, - { - actions: [ - assign({ resource: ({ event }) => event.output }), - raise(({ event }) => ({ type: 'NEXT', resource: event.output })), - ], - }, - ], - onError: { - actions: 'setFormErrors', - target: 'Pending', - }, - }, - }, - Pending: { - tags: ['state:pending'], - on: { - RETRY: 'Preparing', - SUBMIT: { - target: 'Attempting', - reenter: true, - }, - }, - initial: 'NotResendable', - states: { - Resendable: { - description: 'Waiting for user to retry', - }, - NotResendable: { - description: 'Handle countdowns', - on: { - RETRY: { - actions: log(({ context }) => `Not retriable; Try again in ${context.resendableAfter}s`), - }, - }, - after: { - resendableTimeout: [ - { - description: 'Set as retriable if countdown is 0', - guard: 'isResendable', - actions: 'resendableTick', - target: 'Resendable', - }, - { - description: 'Continue countdown if not retriable', - actions: 'resendableTick', - target: 'NotResendable', - reenter: true, - }, - ], - }, - }, - }, - }, - Attempting: { - tags: ['state:attempting', 'state:loading'], - entry: 'sendToLoading', - invoke: { - id: 'attemptPhoneNumberVerification', - src: 'attempt', - input: ({ context }) => ({ - parent: context.parent, - params: { - strategy: 'phone_code', - code: (context.formRef.getSnapshot().context.fields.get('code')?.value as string) || '', - }, - }), - onDone: { - actions: [raise(({ event }) => ({ type: 'NEXT', resource: event.output })), 'sendToLoading'], - }, - onError: { - actions: ['setFormErrors', 'sendToLoading'], - target: 'Pending', - }, - }, - }, - }, - }, - }, -}); diff --git a/packages/elements/src/internals/machines/sign-up/verification.types.ts b/packages/elements/src/internals/machines/sign-up/verification.types.ts deleted file mode 100644 index edf46bf35f9..00000000000 --- a/packages/elements/src/internals/machines/sign-up/verification.types.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { ClerkAPIResponseError } from '@clerk/shared/error'; -import type { Attributes, SignUpResource } from '@clerk/types'; -import type { ActorRefFrom, DoneActorEvent, ErrorActorEvent } from 'xstate'; - -import type { FormMachine } from '~/internals/machines/form'; - -import type { SignInRouterMachineActorRef } from './router.types'; - -// ---------------------------------- Tags ---------------------------------- // - -export type SignUpVerificationStateTags = 'state:preparing' | 'state:pending' | 'state:attempting' | 'state:loading'; - -export type SignUpVerificationVerificationCategoryTags = 'verification:category:code' | 'verification:category:link'; -export type SignUpVerificationVerificationMethodTags = 'verification:method:email' | 'verification:method:phone'; -export type SignUpVerificationVerificationTypeTags = - | 'verification:email_link' - | 'verification:email_code' - | 'verification:phone_code'; - -export type SignUpVerificationVerificationTags = - | SignUpVerificationVerificationCategoryTags - | SignUpVerificationVerificationMethodTags - | SignUpVerificationVerificationTypeTags; - -export type SignUpVerificationTags = SignUpVerificationStateTags | SignUpVerificationVerificationTags; -export type SignUpVerificationFriendlyTags = 'code' | 'email_link' | 'email_code' | 'phone_code'; - -// ---------------------------------- Events ---------------------------------- // - -export type SignUpVerificationSubmitEvent = { type: 'SUBMIT'; action: 'submit' }; -export type SignUpVerificationNextEvent = { type: 'NEXT'; resource?: SignUpResource }; -export type SignUpVerificationRetryEvent = { type: 'RETRY' }; - -export type SignUpVerificationEmailLinkVerifiedEvent = { type: 'EMAIL_LINK.VERIFIED'; resource: SignUpResource }; -export type SignUpVerificationEmailLinkUnverifiedEvent = { type: 'EMAIL_LINK.UNVERIFIED'; resource: SignUpResource }; -export type SignUpVerificationEmailLinkExpiredEvent = { type: 'EMAIL_LINK.EXPIRED'; resource: SignUpResource }; -export type SignUpVerificationEmailLinkTransferrableEvent = { - type: 'EMAIL_LINK.TRANSFERRABLE'; - resource: SignUpResource; -}; -export type SignUpVerificationEmailLinkRestartEvent = { type: 'EMAIL_LINK.RESTART' }; -export type SignUpVerificationEmailLinkFailedEvent = { - type: 'EMAIL_LINK.FAILED'; - resource: SignUpResource; - error: Error; -}; - -export type SignUpVerificationEmailLinkEvent = - | SignUpVerificationEmailLinkVerifiedEvent - | SignUpVerificationEmailLinkUnverifiedEvent - | SignUpVerificationEmailLinkExpiredEvent - | SignUpVerificationEmailLinkRestartEvent - | SignUpVerificationEmailLinkFailedEvent; - -export type SignUpVerificationEvents = - | DoneActorEvent - | ErrorActorEvent - | SignUpVerificationRetryEvent - | SignUpVerificationSubmitEvent - | SignUpVerificationNextEvent - | SignUpVerificationEmailLinkEvent; - -// ---------------------------------- Input ---------------------------------- // - -export type SignUpVerificationInput = { - attributes: Attributes | undefined; - basePath?: string; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - resource: SignUpResource; -}; - -// ---------------------------------- Delays ---------------------------------- // - -export const SignUpVerificationDelays = { - emailLinkTimeout: 300_000, // 5 minutes - resendableTimeout: 1_000, // 1 second -} as const; - -export type SignUpVerificationDelays = keyof typeof SignUpVerificationDelays; - -// ---------------------------------- Context ---------------------------------- // - -export interface SignUpVerificationContext { - attributes: Attributes | undefined; - basePath: string; - resource: SignUpResource; - error?: Error | ClerkAPIResponseError; - formRef: ActorRefFrom; - parent: SignInRouterMachineActorRef; - loadingStep: 'verifications'; - resendable: boolean; - resendableAfter: number; -} - -// ---------------------------------- Schema ---------------------------------- // - -export interface SignUpVerificationSchema { - context: SignUpVerificationContext; - delays: SignUpVerificationDelays; - input: SignUpVerificationInput; - events: SignUpVerificationEvents; - tags: SignUpVerificationTags; -} diff --git a/packages/elements/src/internals/machines/third-party/index.ts b/packages/elements/src/internals/machines/third-party/index.ts deleted file mode 100644 index 711a35e90f4..00000000000 --- a/packages/elements/src/internals/machines/third-party/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './third-party.actors'; -export * from './third-party.machine'; -export * from './third-party.types'; diff --git a/packages/elements/src/internals/machines/third-party/third-party.actors.ts b/packages/elements/src/internals/machines/third-party/third-party.actors.ts deleted file mode 100644 index e80a48b2e9c..00000000000 --- a/packages/elements/src/internals/machines/third-party/third-party.actors.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { - AuthenticateWithRedirectParams, - HandleOAuthCallbackParams, - HandleSamlCallbackParams, - LoadedClerk, -} from '@clerk/types'; -import type { SetOptional } from 'type-fest'; -import type { AnyActorRef, AnyEventObject } from 'xstate'; -import { fromCallback, fromPromise } from 'xstate'; - -import { ClerkElementsRuntimeError } from '~/internals/errors'; -import type { WithParams, WithUnsafeMetadata } from '~/internals/machines/shared'; -import { ClerkJSNavigationEvent, isClerkJSNavigationEvent } from '~/internals/machines/utils/clerkjs'; - -type OptionalRedirectParams = 'redirectUrl' | 'redirectUrlComplete'; - -export type AuthenticateWithRedirectSignInParams = SetOptional; -export type AuthenticateWithRedirectSignUpParams = SetOptional< - WithUnsafeMetadata, - OptionalRedirectParams ->; - -export type AuthenticateWithRedirectInput = ( - | (WithParams & { flow: 'signIn' }) - | (WithParams & { flow: 'signUp' }) -) & { basePath: string; parent: AnyActorRef }; // TODO: Fix circular dependency - -export const redirect = fromPromise( - async ({ input: { flow, params, parent } }) => { - const clerk: LoadedClerk = parent.getSnapshot().context.clerk; - - return clerk.client[flow].authenticateWithRedirect({ - redirectUrl: clerk.buildUrlWithAuth(params.redirectUrl || '/'), - redirectUrlComplete: clerk.buildUrlWithAuth(params.redirectUrlComplete || '/'), - ...params, - }); - }, -); - -export type HandleRedirectCallbackParams> = { - [K in keyof T]: NonNullable; -}; - -export type HandleRedirectCallbackInput = AnyActorRef; - -/** - * This function hijacks handleRedirectCallback from ClerkJS to handle navigation events - * from the state machine. - */ -export const handleRedirectCallback = fromCallback( - ({ sendBack, input: parent }) => { - const clerk: LoadedClerk = parent.getSnapshot().context.clerk; - const displayConfig = clerk.__unstable__environment?.displayConfig; - - const customNavigate = (toEvt: string) => { - const to = toEvt.split('/').slice(-1)[0]; - - if (isClerkJSNavigationEvent(to)) { - // Handle known redefined navigation events - sendBack({ type: to }); - } else if (to === displayConfig?.signInUrl) { - // Handle known non-redefined sign-in navigation events - sendBack({ type: ClerkJSNavigationEvent.signIn }); - } else if (to === displayConfig?.signUpUrl) { - // Handle known non-redefined sign-up navigation events - sendBack({ type: ClerkJSNavigationEvent.signUp }); - } else { - // Handle unknown navigation events - sendBack({ type: 'FAILURE', error: new ClerkElementsRuntimeError(`Unknown navigation event: ${to}`) }); - } - - return Promise.resolve(); - }; - - // @ts-expect-error - Clerk types are incomplete - // TODO: Update local Clerk types - const loadedClerk = (clerk.clerkjs ?? clerk) as LoadedClerk; - - void loadedClerk.handleRedirectCallback( - { - continueSignUpUrl: ClerkJSNavigationEvent.continue, - firstFactorUrl: ClerkJSNavigationEvent.signIn, - resetPasswordUrl: ClerkJSNavigationEvent.resetPassword, - secondFactorUrl: ClerkJSNavigationEvent.signIn, - verifyEmailAddressUrl: ClerkJSNavigationEvent.verification, - verifyPhoneNumberUrl: ClerkJSNavigationEvent.verification, - signUpUrl: ClerkJSNavigationEvent.signUp, - signInUrl: ClerkJSNavigationEvent.signIn, - } satisfies HandleOAuthCallbackParams, - customNavigate, - ); - - return () => void 0; - }, -); diff --git a/packages/elements/src/internals/machines/third-party/third-party.machine.ts b/packages/elements/src/internals/machines/third-party/third-party.machine.ts deleted file mode 100644 index fbefe674277..00000000000 --- a/packages/elements/src/internals/machines/third-party/third-party.machine.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { assertEvent, assign, log, not, sendTo, setup } from 'xstate'; - -import { sendToLoading } from '~/internals/machines/shared'; -import { assertActorEventError } from '~/internals/machines/utils/assert'; - -import { handleRedirectCallback, redirect } from './third-party.actors'; -import type { ThirdPartyMachineSchema } from './third-party.types'; - -export const ThirdPartyMachineId = 'ThirdParty'; - -export type TThirdPartyMachine = typeof ThirdPartyMachine; - -export const ThirdPartyMachine = setup({ - actors: { - handleRedirectCallback, - redirect, - }, - actions: { - logError: log(({ event }) => `Error: ${event.type}`), - assignActiveStrategy: assign({ - activeStrategy: ({ event }) => { - assertEvent(event, 'REDIRECT'); - return event.params.strategy; - }, - }), - unassignActiveStrategy: assign({ - activeStrategy: null, - }), - sendToNext: ({ context }) => context.parent.send({ type: 'NEXT' }), - sendToLoading, - setFormErrors: sendTo( - ({ context }) => context.formRef, - ({ event }) => { - assertActorEventError(event); - return { - type: 'ERRORS.SET', - error: event.error, - }; - }, - ), - }, - guards: { - isExampleMode: ({ context }) => Boolean(context.parent.getSnapshot().context.exampleMode), - }, - types: {} as ThirdPartyMachineSchema, -}).createMachine({ - id: ThirdPartyMachineId, - context: ({ input }) => ({ - activeStrategy: null, - basePath: input.basePath, - formRef: input.formRef, - flow: input.flow, - parent: input.parent, - loadingStep: 'strategy', - }), - initial: 'Idle', - states: { - Idle: { - description: 'Sets third-party providers if not already set, and waits for a redirect or callback event', - on: { - CALLBACK: 'HandlingCallback', - REDIRECT: { - guard: not('isExampleMode'), - target: 'Redirecting', - reenter: true, - }, - }, - }, - Redirecting: { - description: 'Redirects to the third-party provider for authentication', - tags: ['state:redirect', 'state:loading'], - entry: ['assignActiveStrategy', 'sendToLoading'], - exit: ['unassignActiveStrategy', 'sendToLoading'], - invoke: { - id: 'redirect', - src: 'redirect', - input: ({ context, event }) => { - assertEvent(event, 'REDIRECT'); - - const legalAcceptedField = context.formRef.getSnapshot().context.fields.get('legalAccepted')?.checked; - - return { - basePath: context.basePath, - flow: context.flow, - params: { - ...event.params, - legalAccepted: legalAcceptedField || undefined, - }, - parent: context.parent, - }; - }, - onError: { - actions: 'setFormErrors', - target: 'Idle', - }, - }, - }, - HandlingCallback: { - description: 'Handles the callback from the third-party provider', - tags: ['state:callback', 'state:loading'], - invoke: { - id: 'handleRedirectCallback', - src: 'handleRedirectCallback', - input: ({ context }) => context.parent, - onError: { - actions: ['logError', 'setFormErrors'], - target: 'Idle', - }, - }, - on: { - 'CLERKJS.NAVIGATE.*': { - actions: 'sendToNext', - target: 'Idle', - }, - }, - }, - }, -}); diff --git a/packages/elements/src/internals/machines/third-party/third-party.types.ts b/packages/elements/src/internals/machines/third-party/third-party.types.ts deleted file mode 100644 index 328e2efaaa6..00000000000 --- a/packages/elements/src/internals/machines/third-party/third-party.types.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { AuthenticateWithRedirectParams } from '@clerk/types'; -import type { SetOptional } from 'type-fest'; -import type { ActorRefFrom, AnyActorRef } from 'xstate'; - -import type { ClerkJSNavigationEvent } from '~/internals/machines/utils/clerkjs'; - -import type { TFormMachine } from '../form'; - -type Flow = 'signIn' | 'signUp'; - -// ================= Schema ================= // - -export interface ThirdPartyMachineSchema { - context: ThirdPartyMachineContext; - input: ThirdPartyMachineInput; - events: ThirdPartyMachineEvent; -} - -// ================= Context ================= // - -export interface ThirdPartyMachineContext { - /** - * Currently active strategy - * (Can be used for loading states) - */ - activeStrategy: string | null; // TODO: Update type - basePath: string; - flow: Flow; - formRef: ActorRefFrom; - parent: AnyActorRef; // TODO: Fix circular dependency - loadingStep: 'strategy'; -} - -// ================= Input ================= // - -export interface ThirdPartyMachineInput { - basePath: string; - flow: Flow; - formRef: ActorRefFrom; - parent: AnyActorRef; // TODO: Fix circular dependency -} - -// ================= Events ================= // - -export type RedirectEvent = { - type: 'REDIRECT'; - params: SetOptional; -}; -export type RedirectCallbackEvent = { type: 'CALLBACK' }; -export type CallbackNavigationEvent = { type: ClerkJSNavigationEvent }; - -export type ThirdPartyMachineEvent = RedirectEvent | RedirectCallbackEvent | CallbackNavigationEvent; diff --git a/packages/elements/src/internals/machines/types/index.ts b/packages/elements/src/internals/machines/types/index.ts deleted file mode 100644 index 9331557b3ad..00000000000 --- a/packages/elements/src/internals/machines/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './router.types'; diff --git a/packages/elements/src/internals/machines/types/router.types.ts b/packages/elements/src/internals/machines/types/router.types.ts deleted file mode 100644 index 89bd93ee25d..00000000000 --- a/packages/elements/src/internals/machines/types/router.types.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { ClerkRouter } from '@clerk/shared/router'; -import type { - ClerkResource, - EnterpriseSSOStrategy, - LoadedClerk, - OAuthStrategy, - SamlStrategy, - SignInStrategy, - Web3Strategy, -} from '@clerk/types'; -import type { ActorRefFrom } from 'xstate'; - -import type { ClerkElementsError } from '~/internals/errors'; -import type { TFormMachine } from '~/internals/machines/form'; - -// ---------------------------------- Events ---------------------------------- // - -export type BaseRouterLoadingStep = - | 'start' - | 'verifications' - | 'continue' - | 'reset-password' - | 'forgot-password' - | 'choose-strategy' - | 'error'; - -export type BaseRouterNextEvent = { type: 'NEXT'; resource?: T }; -export type BaseRouterFormAttachEvent = { type: 'FORM.ATTACH'; formRef: ActorRefFrom }; -export type BaseRouterPrevEvent = { type: 'NAVIGATE.PREVIOUS' }; -export type BaseRouterStartEvent = { type: 'NAVIGATE.START' }; -export type BaseRouterResetEvent = { type: 'RESET' }; -export type BaseRouterResetStepEvent = { type: 'RESET.STEP' }; -export type BaseRouterErrorEvent = { type: 'ERROR'; error: Error }; -export type BaseRouterTransferEvent = { type: 'TRANSFER' }; -export type BaseRouterLoadingEvent = ( - | { - step: TSteps | undefined; - strategy?: never; - action?: string; - } - | { - step?: never; - strategy: SignInStrategy | undefined; - action?: never; - } -) & { type: 'LOADING'; isLoading: boolean }; - -export type BaseRouterRedirectOauthEvent = { type: 'AUTHENTICATE.OAUTH'; strategy: OAuthStrategy }; -export type BaseRouterRedirectSamlEvent = { type: 'AUTHENTICATE.SAML'; strategy?: SamlStrategy }; -export type BaseRouterRedirectEnterpriseSSOEvent = { - type: 'AUTHENTICATE.ENTERPRISE_SSO'; - strategy?: EnterpriseSSOStrategy; -}; -export type BaseRouterRedirectWeb3Event = { type: 'AUTHENTICATE.WEB3'; strategy: Web3Strategy }; -export type BaseRouterSetClerkEvent = { type: 'CLERK.SET'; clerk: LoadedClerk }; - -export type BaseRouterRedirectEvent = - | BaseRouterRedirectOauthEvent - | BaseRouterRedirectSamlEvent - | BaseRouterRedirectWeb3Event - | BaseRouterRedirectEnterpriseSSOEvent; - -// ---------------------------------- Input ---------------------------------- // - -export interface BaseRouterInput { - clerk: LoadedClerk; - router?: ClerkRouter; - exampleMode?: boolean; -} - -// ---------------------------------- Context ---------------------------------- // - -export interface BaseRouterContext { - clerk: LoadedClerk; - error?: ClerkElementsError; - router?: ClerkRouter; - exampleMode?: boolean; -} diff --git a/packages/elements/src/internals/machines/utils/__tests__/assert.test.ts b/packages/elements/src/internals/machines/utils/__tests__/assert.test.ts deleted file mode 100644 index a0088401120..00000000000 --- a/packages/elements/src/internals/machines/utils/__tests__/assert.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { assertActorEventDone, assertActorEventError, assertIsDefined } from '../assert'; - -describe('assertIsDefined', () => { - it('should throw an error if the value is undefined', () => { - const value = undefined; - expect(() => assertIsDefined(value)).toThrowError('undefined is not defined'); - }); - - it('should throw an error if the value is null', () => { - const value = null; - expect(() => assertIsDefined(value)).toThrowError('null is not defined'); - }); - - it('should not throw an error if the value is defined', () => { - const value = 'Hello'; - expect(() => assertIsDefined(value)).not.toThrowError(); - }); -}); - -describe('assertActorEventError', () => { - it('should throw an error if the event is not an error event', () => { - const event = { type: 'success' }; - expect(() => assertActorEventError(event)).toThrowError('Expected an error event, got "success"'); - }); - - it('should not throw an error if the event is an error event', () => { - const event = { type: 'error', error: new Error('Something went wrong') }; - expect(() => assertActorEventError(event)).not.toThrowError(); - }); -}); - -describe('assertActorEventDone', () => { - it('should throw an error if the event is not a done event', () => { - const event = { type: 'success' }; - expect(() => assertActorEventDone(event)).toThrowError('Expected a done event, got "success"'); - }); - - it('should not throw an error if the event is a done event', () => { - const event = { type: 'done', output: 'Result' }; - expect(() => assertActorEventDone(event)).not.toThrowError(); - }); -}); diff --git a/packages/elements/src/internals/machines/utils/__tests__/formatters.test.ts b/packages/elements/src/internals/machines/utils/__tests__/formatters.test.ts deleted file mode 100644 index b1ee63bc791..00000000000 --- a/packages/elements/src/internals/machines/utils/__tests__/formatters.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { formatName, formatSalutation } from '../formatters'; - -describe('formatName', () => { - test('returns undefined when no arguments are provided', () => { - expect(formatName()).toBeUndefined(); - }); - - test('returns the titleized version of the single argument', () => { - expect(formatName('john')).toBe('John'); - }); - - test('returns the titleized version of multiple arguments joined by space', () => { - expect(formatName('john', 'doe')).toBe('John Doe'); - }); - - test('ignores undefined arguments and returns the titleized version of the rest', () => { - expect(formatName(undefined, 'john', undefined, 'doe')).toBe('John Doe'); - }); -}); - -describe('formatSalutation', () => { - test('returns the formatted salutation based on firstName', () => { - expect(formatSalutation({ firstName: 'John', lastName: undefined, identifier: undefined })).toBe('John'); - }); - - test('returns the formatted salutation based on lastName', () => { - expect(formatSalutation({ firstName: undefined, lastName: 'Doe', identifier: undefined })).toBe('Doe'); - }); - - test('returns the formatted salutation based on identifier', () => { - expect(formatSalutation({ firstName: undefined, lastName: undefined, identifier: 'test@clerk.dev' })).toBe( - 'test@clerk.dev', - ); - }); - - test('returns an empty string when no arguments are provided', () => { - expect(formatSalutation({ firstName: undefined, lastName: undefined, identifier: undefined })).toBe(''); - }); - - test('returns the formatted salutation based on firstName and lastName', () => { - expect(formatSalutation({ firstName: 'John', lastName: 'Doe', identifier: undefined })).toBe('John'); - }); - - test('returns the formatted salutation based on firstName, lastName, and identifier', () => { - expect(formatSalutation({ firstName: 'John', lastName: 'Doe', identifier: 'test@clerk.dev' })).toBe('John'); - }); -}); diff --git a/packages/elements/src/internals/machines/utils/__tests__/next.test.ts b/packages/elements/src/internals/machines/utils/__tests__/next.test.ts deleted file mode 100644 index 611e87a68e2..00000000000 --- a/packages/elements/src/internals/machines/utils/__tests__/next.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NEXT_WINDOW_HISTORY_SUPPORT_VERSION } from '~/internals/constants'; - -import { shouldUseVirtualRouting } from '../next'; - -let windowSpy: jest.SpyInstance; - -beforeEach(() => { - windowSpy = jest.spyOn(globalThis, 'window', 'get'); -}); - -afterEach(() => { - windowSpy.mockRestore(); -}); - -describe('shouldUseVirtualRouting', () => { - it('should return false if window is undefined', () => { - windowSpy.mockReturnValue(undefined); - - expect(shouldUseVirtualRouting()).toBe(false); - }); - it('should return false if window.next is undefined', () => { - windowSpy.mockReturnValue({}); - - expect(shouldUseVirtualRouting()).toBe(false); - }); - it('should return true if version is lower than NEXT_WINDOW_HISTORY_SUPPORT_VERSION', () => { - windowSpy.mockReturnValue({ next: { version: '14.0.0' } }); - - expect(shouldUseVirtualRouting()).toBe(true); - }); - it('should return false if version is NEXT_ROUTING_CHANGE_VERSION', () => { - windowSpy.mockReturnValue({ next: { version: NEXT_WINDOW_HISTORY_SUPPORT_VERSION } }); - - expect(shouldUseVirtualRouting()).toBe(false); - }); - it('should return false if version is higher than NEXT_WINDOW_HISTORY_SUPPORT_VERSION', () => { - windowSpy.mockReturnValue({ next: { version: '14.6.0' } }); - - expect(shouldUseVirtualRouting()).toBe(false); - }); -}); diff --git a/packages/elements/src/internals/machines/utils/__tests__/strategies.test.ts b/packages/elements/src/internals/machines/utils/__tests__/strategies.test.ts deleted file mode 100644 index 189a2049c8c..00000000000 --- a/packages/elements/src/internals/machines/utils/__tests__/strategies.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { matchStrategy } from '../strategies'; - -describe('matchStrategy', () => { - it('should return false if either current or desired is undefined', () => { - expect(matchStrategy(undefined, 'oauth')).toBe(false); - expect(matchStrategy('password', undefined)).toBe(false); - expect(matchStrategy(undefined, undefined)).toBe(false); - }); - - it('should return true if current is equal to desired', () => { - expect(matchStrategy('password', 'password')).toBe(true); - }); - - it('should return true if current partially matches desired', () => { - expect(matchStrategy('oauth_google', 'oauth')).toBe(true); - expect(matchStrategy('web3_metamask_signature', 'web3')).toBe(true); - expect(matchStrategy('web3_metamask_signature', 'web3_metamask')).toBe(true); - }); - - it('should return false on invalid partial matches', () => { - expect(matchStrategy('oauth_google', 'web3')).toBe(false); - expect(matchStrategy('oauth_google', 'oauth_goog')).toBe(false); - }); -}); diff --git a/packages/elements/src/internals/machines/utils/assert.ts b/packages/elements/src/internals/machines/utils/assert.ts deleted file mode 100644 index 0ae136a2698..00000000000 --- a/packages/elements/src/internals/machines/utils/assert.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DoneActorEvent, ErrorActorEvent, EventObject } from 'xstate'; - -export function assertIsDefined(value: T, label?: string): asserts value is NonNullable { - if (value === undefined || value === null) { - throw new Error(`${label || value} is not defined`); - } -} - -export function assertActorEventDone(event: EventObject): asserts event is DoneActorEvent { - if ('output' in event === false) { - throw new Error(`Expected a done event, got "${event.type}"`); - } -} - -export function assertActorEventError(event: EventObject): asserts event is ErrorActorEvent { - if ('error' in event === false) { - throw new Error(`Expected an error event, got "${event.type}"`); - } -} diff --git a/packages/elements/src/internals/machines/utils/clerkjs.ts b/packages/elements/src/internals/machines/utils/clerkjs.ts deleted file mode 100644 index 8afb93772a3..00000000000 --- a/packages/elements/src/internals/machines/utils/clerkjs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Simplify } from 'type-fest'; - -// ================= Types ================= // - -export type ClerkJSEventCategory = 'NAVIGATE'; -export type ClerkJSEvent = `CLERKJS.${T}.${string}`; -export type ClerkJSEventObject = Simplify< - Record> ->; -export type ClerkJSEventExtractCategory = S extends `CLERKJS.${infer T}.${string}` ? T : never; - -// ================= Type Narrowing ================= // - -export function isClerkJSEvent>, E extends ClerkJSEvent>( - eventObj: T, - event: E, -): event is typeof event { - return Object.values(eventObj).includes(event as any); -} - -// ================= ClerkJSNavigationEvent ================= // - -export type ClerkJSNavigationEvent = (typeof ClerkJSNavigationEvent)[keyof typeof ClerkJSNavigationEvent]; -export const ClerkJSNavigationEvent: ClerkJSEventObject<'NAVIGATE'> = { - complete: 'CLERKJS.NAVIGATE.COMPLETE', - signUp: 'CLERKJS.NAVIGATE.SIGN_UP', - continue: 'CLERKJS.NAVIGATE.CONTINUE', - generic: 'CLERKJS.NAVIGATE.GENERIC', - resetPassword: 'CLERKJS.NAVIGATE.RESET_PASSWORD', - signIn: 'CLERKJS.NAVIGATE.SIGN_IN', - verification: 'CLERKJS.NAVIGATE.VERIFICATION', -} as const; - -export function isClerkJSNavigationEvent(event: unknown): event is ClerkJSNavigationEvent { - return isClerkJSEvent(ClerkJSNavigationEvent, event as ClerkJSEvent<'NAVIGATE'>); -} diff --git a/packages/elements/src/internals/machines/utils/formatters.ts b/packages/elements/src/internals/machines/utils/formatters.ts deleted file mode 100644 index 8198ec4e052..00000000000 --- a/packages/elements/src/internals/machines/utils/formatters.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { titleize } from '@clerk/shared/underscore'; - -// TODO: ideally the derivation of these values lives in FAPI and comes back directly from the API - -export function formatName(...args: (string | undefined)[]): string | undefined { - switch (args.length) { - case 0: - return undefined; - case 1: - return titleize(args[0]); - default: - return args.filter(Boolean).map(titleize).join(' '); - } -} - -export function formatSalutation({ - firstName, - lastName, - identifier, -}: { - firstName: string | undefined; - lastName: string | undefined; - identifier: string | undefined | null; -}): string { - return (firstName && formatName(firstName)) || (lastName && formatName(lastName)) || identifier || ''; -} diff --git a/packages/elements/src/internals/machines/utils/next.ts b/packages/elements/src/internals/machines/utils/next.ts deleted file mode 100644 index 69a08cc6bdd..00000000000 --- a/packages/elements/src/internals/machines/utils/next.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NEXT_WINDOW_HISTORY_SUPPORT_VERSION } from '~/internals/constants'; - -export function shouldUseVirtualRouting() { - if (typeof window === 'undefined') { - return false; - } - - if (!window.next) { - return false; - } - - return window.next.version < NEXT_WINDOW_HISTORY_SUPPORT_VERSION; -} diff --git a/packages/elements/src/internals/machines/utils/strategies.ts b/packages/elements/src/internals/machines/utils/strategies.ts deleted file mode 100644 index 5ea580e25ca..00000000000 --- a/packages/elements/src/internals/machines/utils/strategies.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const matchStrategy = (current: string | undefined, desired: string | undefined): boolean => { - if (!current || !desired) { - return false; - } - - if (current === desired) { - return true; - } - - return current.startsWith(`${desired}_`); -}; diff --git a/packages/elements/src/internals/utils/inspector/browser/index.ts b/packages/elements/src/internals/utils/inspector/browser/index.ts deleted file mode 100644 index 30f12a3e821..00000000000 --- a/packages/elements/src/internals/utils/inspector/browser/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isTruthy } from '@clerk/shared/underscore'; -import { createBrowserInspector } from '@statelyai/inspect'; - -import { safeAccess } from '~/utils/safe-access'; - -export const getInspector = () => { - if ( - __DEV__ && - typeof window !== 'undefined' && - process.env.NODE_ENV === 'development' && - isTruthy( - safeAccess(() => process.env.NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG_UI ?? process.env.CLERK_ELEMENTS_DEBUG_UI, false), - ) - ) { - const { inspect } = createBrowserInspector({ - autoStart: true, - }); - - return inspect; - } - - return undefined; -}; diff --git a/packages/elements/src/internals/utils/inspector/console/console.ts b/packages/elements/src/internals/utils/inspector/console/console.ts deleted file mode 100644 index 0f2f6fa2f57..00000000000 --- a/packages/elements/src/internals/utils/inspector/console/console.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { ActorRefLike, AnyEventObject, InspectionEvent, Observer } from 'xstate'; - -let consoleInspector: Observer | undefined; - -export interface ConsoleInspectorOptions { - /** - * Enable server-side debugging - */ - debugServer?: boolean; - /** - * Enable console inspector - */ - enabled: boolean; -} - -export function createConsoleInspector({ - enabled, - debugServer, -}: ConsoleInspectorOptions): Observer | undefined { - if (consoleInspector) { - return consoleInspector; - } - if (!enabled || (!debugServer && typeof window === 'undefined')) { - return undefined; - } - - const parseRefId = (ref: ActorRefLike | undefined, includeSystemId?: false): string | undefined => { - if (!ref) { - return undefined; - } - - // @ts-expect-error - Exists on the ref.src - const id = ref.src?.id; - - // @ts-expect-error - id exists on ActorRefLike - let output = id || ref.id; - - if (includeSystemId) { - // @ts-expect-error - id exists on ActorRefLike - output += `(${ref.id})`; - } - - return output; - }; - - function logEvent(labelOrMsg: any, ...optionalParams: any[]): void { - if (optionalParams && optionalParams.length > 0) { - console.log(`%c${labelOrMsg}%c`, 'font-weight: bold;', 'color: inherit;', ...optionalParams); - } else { - console.log(labelOrMsg); - } - } - - const defaults = 'font-weight: bold; line-height: 1.5; border-radius: 8px; padding: 4px 10px;'; - const reset = 'color: inherit;'; - - const Styles = { - info: { - label: `background: #113264; color: #8EC8F6;`, // blue 12, blue 5 - sublabel: `background: #113264; color: #C2E5FF;`, // blue 12, blue 7 - }, - success: { - label: `background: #203C25; color: #94CE9A;`, // grass 12, grass 5 - sublabel: `background: #203C25; color: #C9E8CA;`, // grass 12, grass 7 - }, - warning: { - label: `background: #473B1F; color: #E4C767;`, // yellow 12, yellow 5 - sublabel: `background: #473B1F; color: #FFE770;`, // yellow 12, yellow 7 - }, - error: { - label: `background: #5C271F; color: #F5A898;`, // tomato 12, tomato 5 - sublabel: `background: #5C271F; color: #FFCDC2;`, // tomato 12, tomato 7 - }, - } as const; - - type Style = keyof typeof Styles; - - const logGroup = ( - { label, sublabel, details, style = 'info' }: { label: string; sublabel?: string; details?: string; style?: Style }, - cb: () => void, - ) => { - const styles = Styles[style]; - - const msg = [`%c${label}%c\t`]; - const params: string[] = [`${defaults} ${styles.label}`, reset]; - - if (sublabel) { - msg.push(`%c${sublabel}%c`); - params.push(`${defaults} ${styles.sublabel}`, reset); - } - - if (details) { - msg.push(`%c${details}`); - params.push(defaults); - } - - console.groupCollapsed(msg.join(''), ...params); - cb(); - console.groupEnd(); - }; - - function determineStyleFromEvent(event: InspectionEvent | AnyEventObject): Style { - switch (event.type) { - case 'ROUTE.REGISTER': - return 'success'; - case 'ROUTE.UNREGISTER': - return 'warning'; - case 'SUBMIT': - return 'info'; - } - - if (event.type.startsWith('xstate.done.')) { - return 'success'; - } else if (event.type.startsWith('xstate.error.')) { - return 'error'; - } - - return 'info'; - } - - consoleInspector = { - next: inspectionEvent => { - if (inspectionEvent.type === '@xstate.actor') { - logGroup({ label: 'ACTOR', sublabel: parseRefId(inspectionEvent.actorRef) }, () => { - logEvent('Actor Ref', inspectionEvent.actorRef); - }); - } - - if (inspectionEvent.type === '@xstate.event') { - logGroup( - { - label: 'EVENT', - sublabel: inspectionEvent.event.type, - details: [parseRefId(inspectionEvent.sourceRef), parseRefId(inspectionEvent.actorRef)] - .filter(Boolean) - .join(' ⮕ '), - style: determineStyleFromEvent(inspectionEvent.event), - }, - () => { - logEvent('Type', inspectionEvent.event.type); - logEvent('Source', inspectionEvent.sourceRef); - logEvent('Actor', inspectionEvent.actorRef); - logEvent('Event', inspectionEvent.event); - }, - ); - } - - if (inspectionEvent.type === '@xstate.snapshot') { - logGroup( - { - label: 'SNAPSHOT', - sublabel: parseRefId(inspectionEvent.actorRef), - style: determineStyleFromEvent(inspectionEvent.event), - }, - () => { - logEvent('Type', inspectionEvent.event.type); - // @ts-expect-error - _parent exists on ActorRefLike - logEvent('Parent', parseRefId(inspectionEvent.actorRef._parent)); - logEvent('Actor', inspectionEvent.actorRef); - logEvent('Event', inspectionEvent.event); - logEvent('Snapshot', inspectionEvent.snapshot); - }, - ); - } - }, - }; - - return consoleInspector; -} diff --git a/packages/elements/src/internals/utils/inspector/console/index.ts b/packages/elements/src/internals/utils/inspector/console/index.ts deleted file mode 100644 index 1af63ce18df..00000000000 --- a/packages/elements/src/internals/utils/inspector/console/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { isTruthy } from '@clerk/shared/underscore'; - -import { safeAccess } from '~/utils/safe-access'; - -import { createConsoleInspector } from './console'; - -export function getInspector() { - if ( - __DEV__ && - process.env.NODE_ENV === 'development' && - isTruthy( - safeAccess(() => process.env.NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG_UI ?? process.env.CLERK_ELEMENTS_DEBUG_UI, false), - ) - ) { - return createConsoleInspector({ - enabled: true, - debugServer: isTruthy(safeAccess(() => process.env.CLERK_ELEMENTS_DEBUG_SERVER, false)), - }); - } - return undefined; -} diff --git a/packages/elements/src/internals/utils/inspector/index.ts b/packages/elements/src/internals/utils/inspector/index.ts deleted file mode 100644 index 8ff89fbdd38..00000000000 --- a/packages/elements/src/internals/utils/inspector/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { InspectionEvent, Observer } from 'xstate'; - -import { getInspector as getBrowserInspector } from './browser'; -import { getInspector as getConsoleInspector } from './console'; - -export let inspect: Observer | undefined; - -if (__DEV__) { - inspect = getBrowserInspector() ?? getConsoleInspector(); -} - -const inspector = { - inspect, -}; - -export default inspector; diff --git a/packages/elements/src/react/common/connections.tsx b/packages/elements/src/react/common/connections.tsx deleted file mode 100644 index 53986bb83ba..00000000000 --- a/packages/elements/src/react/common/connections.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import type { EnterpriseSSOStrategy, OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types'; -import { Slot } from '@radix-ui/react-slot'; -import { createContext, useContext } from 'react'; - -import type { ThirdPartyProvider } from '~/utils/third-party-strategies'; - -import { useThirdPartyProvider } from '../hooks'; -import { SignInRouterCtx } from '../sign-in/context'; -import { SignUpRouterCtx } from '../sign-up/context'; - -export type UseThirdPartyProviderReturn = - | (ThirdPartyProvider & { - events: { - authenticate: (event: React.MouseEvent) => void; - }; - }) - | null; - -export const ConnectionContext = createContext(null); -export const useConnectionContext = () => { - const ctx = useContext(ConnectionContext); - - if (!ctx) { - throw new Error('useConnectionContext must be used within '); - } - - return ctx; -}; - -export interface ConnectionProps extends React.ButtonHTMLAttributes { - asChild?: boolean; - name: OAuthProvider | Web3Provider | SamlStrategy | EnterpriseSSOStrategy; -} - -/** - * Renders a social connection button based on the provided name. If your instance does not have the social connection enabled, this component will throw an error in development. - * - * **Tip:** You can use the `` component to render the social connection icon. - * - * @param {boolean} [asChild] - If true, `` will render as its child element, passing along any necessary props. - * @param {OAuthProvider | Web3Provider} name - The name of the social connection to render. - * - * @example - * - * - * - * Sign in with Google - * - * - * - */ -export function Connection({ asChild, name, ...rest }: ConnectionProps) { - const signInRef = SignInRouterCtx.useActorRef(true); - const signUpRef = SignUpRouterCtx.useActorRef(true); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const provider = useThirdPartyProvider((signInRef || signUpRef)!, name); - - if (!provider) { - return null; - } - - const Comp = asChild ? Slot : 'button'; - const defaultProps = asChild ? {} : { type: 'button' as const }; - - return ( - - - - ); -} - -export interface IconProps extends Omit, 'src'> { - asChild?: boolean; -} - -/** - * `` **must** be used inside ``. By default, `` will render as an `` element with the `src` pointing to the logo of the currently used ``. - * - * @param {boolean} [asChild] - If true, `` will render as its child element, passing along any necessary props. - * - * @example - * - * - * - * - * Sign in with Google - * - * - * - */ -export function Icon({ asChild, ...rest }: IconProps) { - const { iconUrl, name } = useConnectionContext(); - - const Comp = asChild ? Slot : 'img'; - return ( - - ); -} diff --git a/packages/elements/src/react/common/form/field-error.tsx b/packages/elements/src/react/common/form/field-error.tsx deleted file mode 100644 index 605312e0c11..00000000000 --- a/packages/elements/src/react/common/form/field-error.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import type { FormMessageProps as RadixFormMessageProps } from '@radix-ui/react-form'; -import { FormMessage as RadixFormMessage } from '@radix-ui/react-form'; -import { Slot } from '@radix-ui/react-slot'; -import * as React from 'react'; - -import { ClerkElementsRuntimeError } from '~/internals/errors'; -import { isReactFragment } from '~/react/utils/is-react-fragment'; - -import { useFieldContext, useFieldFeedback } from './hooks'; -import type { FormErrorProps } from './types'; - -const DISPLAY_NAME = 'ClerkElementsFieldError'; - -export type FormFieldErrorProps = FormErrorProps; -type FormFieldErrorElement = React.ElementRef; - -/** - * FieldError renders error messages associated with a specific field. By default, the error's message will be rendered in an unstyled ``. Optionally, the `children` prop accepts a function to completely customize rendering. - * - * @param {string} [name] - Used to target a specific field by name when rendering outside of a `` component. - * @param {Function} [children] - A function that receives `message` and `code` as arguments. - * - * @example - * - * - * - * - * @example - * - * - * {({ message, code }) => ( - * {message} - * )} - * - * - */ -export const FieldError = React.forwardRef( - ({ asChild = false, children, code, name, ...rest }, forwardedRef) => { - const fieldContext = useFieldContext(); - const rawFieldName = fieldContext?.name || name; - const fieldName = rawFieldName === 'backup_code' ? 'code' : rawFieldName; - const { feedback } = useFieldFeedback({ name: fieldName }); - - if (!(feedback?.type === 'error')) { - return null; - } - - const error = feedback.message; - - if (!error) { - return null; - } - - const Comp = asChild ? Slot : 'span'; - const child = typeof children === 'function' ? children(error) : children; - - // const forceMatch = code ? error.code === code : undefined; // TODO: Re-add when Radix Form is updated - - if (isReactFragment(child)) { - throw new ClerkElementsRuntimeError(' cannot render a Fragment as a child.'); - } - - return ( - - {child || error.message} - - ); - }, -); - -FieldError.displayName = DISPLAY_NAME; diff --git a/packages/elements/src/react/common/form/field-state.tsx b/packages/elements/src/react/common/form/field-state.tsx deleted file mode 100644 index 0f7038e37e0..00000000000 --- a/packages/elements/src/react/common/form/field-state.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ClerkElementsFieldError } from '~/internals/errors'; -import type { ErrorCodeOrTuple } from '~/react/utils/generate-password-error-text'; - -import { useFieldContext, useFieldFeedback, useFieldState, useValidityStateContext } from './hooks'; -import type { FieldStates } from './types'; -import { enrichFieldState } from './utils'; - -type FieldStateRenderFn = { - children: (state: { - state: FieldStates; - message: string | undefined; - codes: ErrorCodeOrTuple[] | undefined; - }) => React.ReactNode; -}; - -const DISPLAY_NAME = 'ClerkElementsFieldState'; - -/** - * Programmatically access the state of the wrapping ``. Useful for implementing animations when direct access to the state value is necessary. - * - * @param {Function} children - A function that receives `state`, `message`, and `codes` as an argument. `state` will is a union of `"success" | "error" | "idle" | "warning" | "info"`. `message` will be the corresponding message, e.g. error message. `codes` will be an array of keys that were used to generate the password validation messages. This prop is only available when the field is of type `password` and has `validatePassword` set to `true`. - * - * @example - * - * Email - * - * {({ state }) => ( - * - * )} - * - * - * - * @example - * - * Password - * - * - * {({ state, message, codes }) => ( - *
Field state: {state}
- *
Field msg: {message}
- *
Pwd keys: {codes.join(', ')}
- * )} - *
- *
- */ -export function FieldState({ children }: FieldStateRenderFn) { - const field = useFieldContext(); - const { feedback } = useFieldFeedback({ name: field?.name }); - const { state } = useFieldState({ name: field?.name }); - const validity = useValidityStateContext(); - - const message = feedback?.message instanceof ClerkElementsFieldError ? feedback.message.message : feedback?.message; - const codes = feedback?.codes; - - const fieldState = { state: enrichFieldState(validity, state), message, codes }; - - return children(fieldState); -} - -FieldState.displayName = DISPLAY_NAME; diff --git a/packages/elements/src/react/common/form/field.tsx b/packages/elements/src/react/common/form/field.tsx deleted file mode 100644 index ed85455edba..00000000000 --- a/packages/elements/src/react/common/form/field.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import type { Autocomplete } from '@clerk/types'; -import type { FormFieldProps as RadixFormFieldProps } from '@radix-ui/react-form'; -import { Field as RadixField, ValidityState as RadixValidityState } from '@radix-ui/react-form'; -import * as React from 'react'; - -import { useFormStore } from '~/internals/machines/form/form.context'; - -import { FieldContext, useField, useFieldState, ValidityStateContext } from './hooks'; -import type { ClerkFieldId, FieldStates } from './types'; -import { enrichFieldState } from './utils'; - -const DISPLAY_NAME = 'ClerkElementsField'; -const DISPLAY_NAME_INNER = 'ClerkElementsFieldInner'; - -type FormFieldElement = React.ElementRef; -export type FormFieldProps = Omit & { - name: Autocomplete; - alwaysShow?: boolean; - children: React.ReactNode | ((state: FieldStates) => React.ReactNode); -}; - -/** - * Field is used to associate its child elements with a specific input. It automatically handles unique ID generation and associating the contained label and input elements. - * - * @param name - Give your `` a unique name inside the current form. If you choose one of the following names Clerk Elements will automatically set the correct type on the `` element: `emailAddress`, `password`, `phoneNumber`, and `code`. - * @param alwaysShow - Optional. When `true`, the field will always be renydered, regardless of its state. By default, a field is hidden if it's optional or if it's a filled-out required field. - * @param {Function} children - A function that receives `state` as an argument. `state` is a union of `"success" | "error" | "idle" | "warning" | "info"`. - * - * @example - * - * Email - * - * - * - * @example - * - * {(fieldState) => ( - * Email - * - * )} - * - */ -export const Field = React.forwardRef(({ alwaysShow, ...rest }, forwardedRef) => { - const formRef = useFormStore(); - const formCtx = formRef.getSnapshot().context; - // A field is marked as hidden if it's optional OR if it's a filled-out required field - const isHiddenField = formCtx.progressive && Boolean(formCtx.hidden?.has(rest.name)); - - // Only alwaysShow={true} should force behavior to render the field, on `undefined` or alwaysShow={false} the isHiddenField logic should take over - const shouldHide = alwaysShow ? false : isHiddenField; - - return shouldHide ? null : ( - - - - ); -}); - -Field.displayName = DISPLAY_NAME; - -const FieldInner = React.forwardRef((props, forwardedRef) => { - const { children, ...rest } = props; - const field = useField({ name: rest.name }); - const { state: fieldState } = useFieldState({ name: rest.name }); - - return ( - - - {validity => { - const enrichedFieldState = enrichFieldState(validity, fieldState); - - return ( - - {typeof children === 'function' ? children(enrichedFieldState) : children} - - ); - }} - - - ); -}); - -FieldInner.displayName = DISPLAY_NAME_INNER; diff --git a/packages/elements/src/react/common/form/form.tsx b/packages/elements/src/react/common/form/form.tsx deleted file mode 100644 index 9f5b67bf740..00000000000 --- a/packages/elements/src/react/common/form/form.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { composeEventHandlers } from '@radix-ui/primitive'; -import type { FormProps as RadixFormProps } from '@radix-ui/react-form'; -import { Form as RadixForm } from '@radix-ui/react-form'; -import * as React from 'react'; -import type { BaseActorRef } from 'xstate'; - -import { useForm } from './hooks'; - -const DISPLAY_NAME = 'ClerkElementsForm'; - -type FormElement = React.ElementRef; -export type FormProps = Omit & { - children: React.ReactNode; - flowActor?: BaseActorRef<{ type: 'SUBMIT'; action: 'submit' }>; -}; - -export const Form = React.forwardRef(({ flowActor, onSubmit, ...rest }, forwardedRef) => { - const form = useForm({ flowActor: flowActor }); - - const { onSubmit: internalOnSubmit, ...internalFormProps } = form.props; - - return ( - - ); -}); - -Form.displayName = DISPLAY_NAME; diff --git a/packages/elements/src/react/common/form/global-error.tsx b/packages/elements/src/react/common/form/global-error.tsx deleted file mode 100644 index 14d92642854..00000000000 --- a/packages/elements/src/react/common/form/global-error.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Slot } from '@radix-ui/react-slot'; -import * as React from 'react'; - -import { ClerkElementsRuntimeError } from '~/internals/errors'; -import { isReactFragment } from '~/react/utils/is-react-fragment'; - -import { useGlobalErrors } from './hooks'; -import type { FormErrorProps } from './types'; - -const DISPLAY_NAME = 'ClerkElementsGlobalError'; - -type FormGlobalErrorElement = React.ElementRef<'div'>; -export type FormGlobalErrorProps = FormErrorProps>; - -/** - * Used to render errors that are returned from Clerk's API, but that are not associated with a specific form field. By default, will render the error's message wrapped in a `
`. Optionally, the `children` prop accepts a function to completely customize rendering. Must be placed **inside** components like ``/`` to have access to the underlying form state. - * - * @param {string} [code] - Forces the message with the matching code to be shown. This is useful when using server-side validation. - * @param {Function} [children] - A function that receives `message` and `code` as arguments. - * @param {boolean} [asChild] - If `true`, `` will render as its child element, passing along any necessary props. - * - * @example - * - * - * - * - * @example - * - * Your custom error message. - * - * - * @example - * - * - * {({ message, code }) => ( - * {message} - * )} - * - * - */ -export const GlobalError = React.forwardRef( - ({ asChild = false, children, code, ...rest }, forwardedRef) => { - const { errors } = useGlobalErrors(); - - const error = errors?.[0]; - - if (!error || (code && error.code !== code)) { - return null; - } - - const Comp = asChild ? Slot : 'div'; - const child = typeof children === 'function' ? children(error) : children; - - if (isReactFragment(child)) { - throw new ClerkElementsRuntimeError(' cannot render a Fragment as a child.'); - } - - return ( - - {child || error.message} - - ); - }, -); - -GlobalError.displayName = DISPLAY_NAME; diff --git a/packages/elements/src/react/common/form/hooks/__tests__/use-field-feedback.test.ts b/packages/elements/src/react/common/form/hooks/__tests__/use-field-feedback.test.ts deleted file mode 100644 index 312a2e65257..00000000000 --- a/packages/elements/src/react/common/form/hooks/__tests__/use-field-feedback.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { renderHook } from '@testing-library/react'; - -import * as internalFormHooks from '~/internals/machines/form/form.context'; - -import { useFieldFeedback } from '../use-field-feedback'; - -type Props = Parameters[0]; - -describe('useFieldFeedback', () => { - it('should correctly output feedback', () => { - const initialProps = { name: 'foo' }; - const returnValue = { codes: 'bar', message: 'baz', type: 'error' }; - - jest.spyOn(internalFormHooks, 'useFormSelector').mockReturnValue(returnValue); - - const { result } = renderHook((props: Props) => useFieldFeedback(props), { initialProps }); - - expect(result.current).toEqual({ feedback: returnValue }); - }); -}); diff --git a/packages/elements/src/react/common/form/hooks/__tests__/use-form.test.tsx b/packages/elements/src/react/common/form/hooks/__tests__/use-form.test.tsx deleted file mode 100644 index 889c330fce1..00000000000 --- a/packages/elements/src/react/common/form/hooks/__tests__/use-form.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { fireEvent, render, renderHook } from '@testing-library/react'; -import { createActor, createMachine } from 'xstate'; - -import { ClerkElementsError } from '~/internals/errors'; - -import { useForm } from '../use-form'; -import * as errorHooks from '../use-global-errors'; - -describe('useForm', () => { - const machine = createMachine({ - on: { - RESET: '.idle', - }, - initial: 'idle', - states: { - idle: { - on: { - SUBMIT: 'success', - }, - }, - success: {}, - }, - }); - - const actor = createActor(machine).start(); - - beforeEach(() => { - actor.send({ type: 'RESET' }); - }); - - it('should correctly output props (no errors)', () => { - jest.spyOn(errorHooks, 'useGlobalErrors').mockReturnValue({ errors: [] }); - - const { result } = renderHook(() => useForm({ flowActor: actor })); - - expect(result.current).toEqual({ - props: { - onSubmit: expect.any(Function), - }, - }); - }); - - it('should correctly output props (has errors)', () => { - jest.spyOn(errorHooks, 'useGlobalErrors').mockReturnValue({ - errors: [new ClerkElementsError('email-link-verification-failed', 'Email verification failed')], - }); - - const { result } = renderHook(() => useForm({ flowActor: actor })); - - expect(result.current).toEqual({ - props: { - 'data-global-error': true, - onSubmit: expect.any(Function), - }, - }); - }); - - it('should create an onSubmit handler', () => { - jest.spyOn(errorHooks, 'useGlobalErrors').mockReturnValue({ errors: [] }); - - const { result } = renderHook(() => useForm({ flowActor: actor })); - const { getByTestId } = render( - , - ); - - fireEvent.submit(getByTestId('form')); - - expect(actor.getSnapshot().value).toEqual('success'); - }); -}); diff --git a/packages/elements/src/react/common/form/hooks/__tests__/use-global-errors.test.ts b/packages/elements/src/react/common/form/hooks/__tests__/use-global-errors.test.ts deleted file mode 100644 index c322f1e2d8a..00000000000 --- a/packages/elements/src/react/common/form/hooks/__tests__/use-global-errors.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { renderHook } from '@testing-library/react'; - -import { ClerkElementsError } from '~/internals/errors'; -import * as internalFormHooks from '~/internals/machines/form/form.context'; - -import { useGlobalErrors } from '../use-global-errors'; - -describe('useGlobalErrors', () => { - it('should correctly output errors (no errors)', () => { - const returnValue: ClerkElementsError[] = []; - - jest.spyOn(internalFormHooks, 'useFormSelector').mockReturnValue([]); - - const { result } = renderHook(() => useGlobalErrors()); - - expect(result.current).toEqual({ errors: returnValue }); - }); - - it('should correctly output errors (has errors)', () => { - const returnValue = [new ClerkElementsError('email-link-verification-failed', 'Email verification failed')]; - - jest.spyOn(internalFormHooks, 'useFormSelector').mockReturnValue(returnValue); - - const { result } = renderHook(() => useGlobalErrors()); - - expect(result.current).toEqual({ errors: returnValue }); - }); -}); diff --git a/packages/elements/src/react/common/form/hooks/__tests__/use-previous.test.ts b/packages/elements/src/react/common/form/hooks/__tests__/use-previous.test.ts deleted file mode 100644 index 50cec13a168..00000000000 --- a/packages/elements/src/react/common/form/hooks/__tests__/use-previous.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { renderHook } from '@testing-library/react'; - -import { usePrevious } from '../use-previous'; - -describe('usePrevious', () => { - it('should retain the previous value', () => { - const { result, rerender } = renderHook((props: string) => usePrevious(props), { initialProps: 'foo' }); - expect(result.current).toBeUndefined(); - - rerender('bar'); - expect(result.current).toBe('foo'); - - rerender('baz'); - expect(result.current).toBe('bar'); - }); -}); diff --git a/packages/elements/src/react/common/form/hooks/index.ts b/packages/elements/src/react/common/form/hooks/index.ts deleted file mode 100644 index 3086f41f44b..00000000000 --- a/packages/elements/src/react/common/form/hooks/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { useField } from './use-field'; -export { useFieldContext, FieldContext } from './use-field-context'; -export { useFieldFeedback } from './use-field-feedback'; -export { useFieldState } from './use-field-state'; -export { useForm } from './use-form'; -export { useGlobalErrors } from './use-global-errors'; -export { useInput } from './use-input'; -export { usePrevious } from './use-previous'; -export { useValidityStateContext, ValidityStateContext } from './use-validity-state-context'; diff --git a/packages/elements/src/react/common/form/hooks/use-field-context.ts b/packages/elements/src/react/common/form/hooks/use-field-context.ts deleted file mode 100644 index 7ffb519dc69..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-field-context.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from 'react'; - -import type { FieldDetails } from '~/internals/machines/form'; - -export const FieldContext = React.createContext | null>(null); -export const useFieldContext = () => React.useContext(FieldContext); diff --git a/packages/elements/src/react/common/form/hooks/use-field-feedback.ts b/packages/elements/src/react/common/form/hooks/use-field-feedback.ts deleted file mode 100644 index 86b9e49f830..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-field-feedback.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type FieldDetails, fieldFeedbackSelector, useFormSelector } from '~/internals/machines/form'; - -export function useFieldFeedback({ name }: Partial>) { - const feedback = useFormSelector(fieldFeedbackSelector(name)); - - return { - feedback, - }; -} diff --git a/packages/elements/src/react/common/form/hooks/use-field-state.ts b/packages/elements/src/react/common/form/hooks/use-field-state.ts deleted file mode 100644 index a30b8cf44b4..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-field-state.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type FieldDetails, fieldHasValueSelector, useFormSelector } from '~/internals/machines/form'; - -import { FIELD_STATES, type FieldStates } from '../types'; -import { useFieldFeedback } from './use-field-feedback'; - -/** - * Given a field name, determine the current state of the field - */ -export function useFieldState({ name }: Partial>) { - const { feedback } = useFieldFeedback({ name }); - const hasValue = useFormSelector(fieldHasValueSelector(name)); - - /** - * If hasValue is false, the state should be idle - * The rest depends on the feedback type - */ - let state: FieldStates = FIELD_STATES.idle; - - if (!hasValue) { - state = FIELD_STATES.idle; - } - - switch (feedback?.type) { - case 'error': - state = FIELD_STATES.error; - break; - case 'warning': - state = FIELD_STATES.warning; - break; - case 'info': - state = FIELD_STATES.info; - break; - case 'success': - state = FIELD_STATES.success; - break; - default: - break; - } - - return { - state, - }; -} diff --git a/packages/elements/src/react/common/form/hooks/use-field.ts b/packages/elements/src/react/common/form/hooks/use-field.ts deleted file mode 100644 index e12f93123e1..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-field.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type FieldDetails, fieldHasValueSelector, useFormSelector } from '~/internals/machines/form'; - -import { useFieldFeedback } from './use-field-feedback'; - -export function useField({ name }: Partial>) { - const hasValue = useFormSelector(fieldHasValueSelector(name)); - const { feedback } = useFieldFeedback({ name }); - - const shouldBeHidden = false; // TODO: Implement clerk-js utils - const hasError = feedback ? feedback.type === 'error' : false; - - return { - hasValue, - props: { - 'data-hidden': shouldBeHidden ? true : undefined, - serverInvalid: hasError, - }, - }; -} diff --git a/packages/elements/src/react/common/form/hooks/use-form.ts b/packages/elements/src/react/common/form/hooks/use-form.ts deleted file mode 100644 index 7213241551a..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-form.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback } from 'react'; -import type { BaseActorRef } from 'xstate'; - -import { useGlobalErrors } from './use-global-errors'; - -/** - * Provides the form submission handler along with the form's validity via a data attribute - */ -export function useForm({ flowActor }: { flowActor?: BaseActorRef<{ type: 'SUBMIT'; action: 'submit' }> }) { - const { errors } = useGlobalErrors(); - - // Register the onSubmit handler for form submission - // TODO: merge user-provided submit handler - const onSubmit = useCallback( - (event: React.FormEvent) => { - event.preventDefault(); - if (flowActor) { - flowActor.send({ type: 'SUBMIT', action: 'submit' }); - } - }, - [flowActor], - ); - - return { - props: { - ...(errors.length > 0 ? { 'data-global-error': true } : {}), - onSubmit, - }, - }; -} diff --git a/packages/elements/src/react/common/form/hooks/use-global-errors.ts b/packages/elements/src/react/common/form/hooks/use-global-errors.ts deleted file mode 100644 index 3b28a13784a..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-global-errors.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { globalErrorsSelector, useFormSelector } from '~/internals/machines/form'; - -export function useGlobalErrors() { - const errors = useFormSelector(globalErrorsSelector); - - return { - errors, - }; -} diff --git a/packages/elements/src/react/common/form/hooks/use-input.tsx b/packages/elements/src/react/common/form/hooks/use-input.tsx deleted file mode 100644 index 2500b6eb24b..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-input.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { Control as RadixControl, type FormControlProps } from '@radix-ui/react-form'; -import * as React from 'react'; - -import { ClerkElementsFieldError } from '~/internals/errors'; -import { fieldValueSelector, useFormSelector, useFormStore } from '~/internals/machines/form'; -import { usePassword } from '~/react/hooks/use-password.hook'; - -import type { FormInputProps } from '../index'; -import { OTP_LENGTH_DEFAULT, OTPInput, type OTPInputProps } from '../otp'; -import { determineInputTypeFromName, enrichFieldState } from '../utils'; -import { useFieldContext } from './use-field-context'; -import { useFieldState } from './use-field-state'; -import { usePrevious } from './use-previous'; -import { useValidityStateContext } from './use-validity-state-context'; - -// TODO: DRY -type PasswordInputProps = Exclude & { - validatePassword?: boolean; -}; - -export function useInput({ - name: inputName, - value: providedValue, - checked: providedChecked, - onChange: onChangeProp, - onBlur: onBlurProp, - onFocus: onFocusProp, - type: inputType, - ...passthroughProps -}: FormInputProps) { - // Inputs can be used outside a wrapper if desired, so safely destructure here - const fieldContext = useFieldContext(); - const rawName = inputName || fieldContext?.name; - const name = rawName === 'backup_code' ? 'code' : rawName; // `backup_code` is a special case of `code` - const { state: fieldState } = useFieldState({ name }); - const validity = useValidityStateContext(); - - if (!rawName || !name) { - throw new Error('Clerk: must be wrapped in a component or have a name prop.'); - } - - const ref = useFormStore(); - const [hasPassedValiation, setHasPassedValidation] = React.useState(false); - - const { validatePassword } = usePassword({ - onValidationComplexity: hasPassed => setHasPassedValidation(hasPassed), - onValidationSuccess: () => { - ref.send({ - type: 'FIELD.FEEDBACK.SET', - field: { name, feedback: { type: 'success', message: 'Your password meets all the necessary requirements.' } }, - }); - }, - onValidationError: (error, codes) => { - if (error) { - ref.send({ - type: 'FIELD.FEEDBACK.SET', - field: { - name, - feedback: { - type: 'error', - message: new ClerkElementsFieldError('password-validation-error', error), - codes, - }, - }, - }); - } - }, - onValidationWarning: (warning, codes) => - ref.send({ - type: 'FIELD.FEEDBACK.SET', - field: { name, feedback: { type: 'warning', message: warning, codes } }, - }), - onValidationInfo: (info, codes) => { - // TODO: If input is not focused, make this info an error - ref.send({ - type: 'FIELD.FEEDBACK.SET', - field: { - name, - feedback: { - type: 'info', - message: info, - codes, - }, - }, - }); - }, - }); - - const value = useFormSelector(fieldValueSelector(name)); - const prevValue = usePrevious(value); - const hasValue = Boolean(value); - const type = inputType ?? determineInputTypeFromName(rawName); - let nativeFieldType = type; - let shouldValidatePassword = false; - - if (type === 'password' || type === 'text') { - shouldValidatePassword = Boolean((passthroughProps as PasswordInputProps).validatePassword); - } - - if (nativeFieldType === 'otp' || nativeFieldType === 'backup_code') { - nativeFieldType = 'text'; - } - - // Register the field in the machine context - React.useEffect(() => { - if (!name) { - return; - } - - ref.send({ - type: 'FIELD.ADD', - field: { name, type: nativeFieldType, value: providedValue, checked: providedChecked }, - }); - - return () => ref.send({ type: 'FIELD.REMOVE', field: { name } }); - }, [ref]); // eslint-disable-line react-hooks/exhaustive-deps - - React.useEffect(() => { - if (!name) { - return; - } - - if ( - (type === 'checkbox' && providedChecked !== undefined) || - (type !== 'checkbox' && providedValue !== undefined) - ) { - ref.send({ - type: 'FIELD.UPDATE', - field: { name, value: providedValue, checked: providedChecked }, - }); - } - }, [name, type, ref, providedValue, providedChecked]); - - // Register the onChange handler for field updates to persist to the machine context - const onChange = React.useCallback( - (event: React.ChangeEvent) => { - onChangeProp?.(event); - if (!name) { - return; - } - ref.send({ type: 'FIELD.UPDATE', field: { name, value: event.target.value, checked: event.target.checked } }); - if (shouldValidatePassword) { - validatePassword(event.target.value); - } - }, - [ref, name, onChangeProp, shouldValidatePassword, validatePassword], - ); - - const onBlur = React.useCallback( - (event: React.FocusEvent) => { - onBlurProp?.(event); - if (shouldValidatePassword && event.target.value !== prevValue) { - validatePassword(event.target.value); - } - }, - [onBlurProp, shouldValidatePassword, validatePassword, prevValue], - ); - - const onFocus = React.useCallback( - (event: React.FocusEvent) => { - onFocusProp?.(event); - if (shouldValidatePassword && event.target.value !== prevValue) { - validatePassword(event.target.value); - } - }, - [onFocusProp, shouldValidatePassword, validatePassword, prevValue], - ); - - // TODO: Implement clerk-js utils - const shouldBeHidden = false; - - const Element = type === 'otp' ? OTPInput : RadixControl; - - let props = {}; - if (type === 'otp') { - const p = passthroughProps as Omit; - const length = p.length || OTP_LENGTH_DEFAULT; - - props = { - 'data-otp-input': true, - autoComplete: 'one-time-code', - inputMode: 'numeric', - pattern: `[0-9]{${length}}`, - minLength: length, - maxLength: length, - // Enhanced naming for better password manager detection - name: 'otp', - id: 'otp-input', - // Additional attributes for password manager compatibility - 'data-testid': 'otp-input', - role: 'textbox', - 'aria-label': 'Enter verification code', - onChange: (event: React.ChangeEvent) => { - // Only accept numbers - event.currentTarget.value = event.currentTarget.value.replace(/\D+/g, ''); - onChange(event); - }, - type: 'text', - spellCheck: false, - }; - } else if (type === 'backup_code') { - props = { - autoComplete: 'off', - type: 'text', - spellCheck: false, - }; - } else if (type === 'password' && shouldValidatePassword) { - props = { - 'data-has-passed-validation': hasPassedValiation ? true : undefined, - }; - } - - // Filter out invalid props that should not be passed through - // @ts-expect-error - Doesn't know about type narrowing by type here - const { validatePassword: _1, ...rest } = passthroughProps; - - return { - Element, - props: { - type, - value: value ?? '', - onChange, - onBlur, - onFocus, - 'data-hidden': shouldBeHidden ? true : undefined, - 'data-has-value': hasValue ? true : undefined, - 'data-state': enrichFieldState(validity, fieldState), - ...props, - ...rest, - }, - }; -} diff --git a/packages/elements/src/react/common/form/hooks/use-previous.ts b/packages/elements/src/react/common/form/hooks/use-previous.ts deleted file mode 100644 index e1bb334fbbc..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-previous.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; - -export function usePrevious(value: T): T | undefined { - const ref = React.useRef(); - - React.useEffect(() => { - ref.current = value; - }, [value]); - - return ref.current; -} diff --git a/packages/elements/src/react/common/form/hooks/use-validity-state-context.ts b/packages/elements/src/react/common/form/hooks/use-validity-state-context.ts deleted file mode 100644 index f95cb49f2ac..00000000000 --- a/packages/elements/src/react/common/form/hooks/use-validity-state-context.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as React from 'react'; - -export const ValidityStateContext = React.createContext(undefined); -export const useValidityStateContext = () => React.useContext(ValidityStateContext); diff --git a/packages/elements/src/react/common/form/index.tsx b/packages/elements/src/react/common/form/index.tsx deleted file mode 100644 index 29e8641e41a..00000000000 --- a/packages/elements/src/react/common/form/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -export type { FormControlProps } from '@radix-ui/react-form'; - -export { Field } from './field'; -export { FieldError } from './field-error'; -export { FieldState } from './field-state'; -export { Form } from './form'; -export { GlobalError } from './global-error'; -export { Input } from './input'; -export { Label } from './label'; -export { Submit } from './submit'; - -export type { FormProps } from './form'; -export type { FormInputProps } from './input'; -export type { FormFieldProps } from './field'; -export type { FormFieldErrorProps } from './field-error'; -export type { FormGlobalErrorProps } from './global-error'; -export type { FormSubmitProps } from './submit'; -export type { FormErrorProps, FormErrorRenderProps } from './types'; diff --git a/packages/elements/src/react/common/form/input.tsx b/packages/elements/src/react/common/form/input.tsx deleted file mode 100644 index 07a09af1c50..00000000000 --- a/packages/elements/src/react/common/form/input.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { logger } from '@clerk/shared/logger'; -import { useClerk } from '@clerk/shared/react'; -import { eventComponentMounted } from '@clerk/shared/telemetry'; -import type { - Control as RadixControl, - FormControlProps, - FormControlProps as RadixFormControlProps, -} from '@radix-ui/react-form'; -import * as React from 'react'; - -import { SignInRouterCtx } from '~/react/sign-in/context'; -import { useSignInPasskeyAutofill } from '~/react/sign-in/context/router.context'; - -import { useInput } from './hooks'; -import type { OTPInputProps } from './otp'; - -const DISPLAY_NAME = 'ClerkElementsInput'; -const DISPLAY_NAME_PASSKEYS_AUTOFILL = 'ClerkElementsInputWithPasskeyAutofill'; - -type PasswordInputProps = Exclude & { - validatePassword?: boolean; -}; - -export type FormInputProps = - | RadixFormControlProps - | ({ type: 'otp'; render: OTPInputProps['render'] } & Omit) - | ({ type: 'otp'; render?: undefined } & OTPInputProps) - // Usecase: Toggle the visibility of the password input, therefore 'password' and 'text' are allowed - | ({ type: 'password' | 'text' } & PasswordInputProps); - -/** - * Handles rendering of `` elements within Clerk's flows. Supports special `type` prop values to render input types that are unique to authentication and user management flows. Additional props will be passed through to the `` element. - * - * @param {boolean} [asChild] - If true, `` will render as its child element, passing along any necessary props. - * @param {string} [name] - Used to target a specific field by name when rendering outside of a `` component. - * - * @example - * - * Email - * - * - * - * @param {Number} [length] - The length of the OTP input. Defaults to 6. - * @param {Number} [passwordManagerOffset] - Password managers place their icon inside an ``. This default behaviour is not desirable when you use the render prop to display N distinct element. With this prop you can increase the width of the `` so that the icon is rendered outside the OTP inputs. - * @param {string} [type] - Type of control to render. Supports a special `'otp'` type for one-time password inputs. If the wrapping `` component has `name='code'`, the type will default to `'otp'`. With the `'otp'` type, the input will have a pattern and length set to 6 by default and render a single `` element. - * - * @example - * - * Email code - * - * - * - * @param {Function} [render] - Optionally, you can use a render prop that controls how each individual character is rendered. If no `render` prop is provided, a single text `` will be rendered. - * - * @example - * - * Email code - * {value}} - * /> - * - */ -export const Input = React.forwardRef, FormInputProps>( - (props: FormInputProps, forwardedRef) => { - const clerk = useClerk(); - const field = useInput(props); - - const hasPasskeyAutofillProp = Boolean(field.props.autoComplete?.includes('webauthn')); - const allowedTypeForPasskey = (['text', 'email', 'tel'] as FormInputProps['type'][]).includes(field.props.type); - const signInRouterRef = SignInRouterCtx.useActorRef(true); - - clerk.telemetry?.record( - eventComponentMounted('Elements_Input', { - type: props.type ?? false, - // @ts-expect-error - Depending on type the props can be different - render: Boolean(props?.render), - // @ts-expect-error - Depending on type the props can be different - asChild: Boolean(props?.asChild), - // @ts-expect-error - Depending on type the props can be different - validatePassword: Boolean(props?.validatePassword), - }), - ); - - if (signInRouterRef && hasPasskeyAutofillProp && allowedTypeForPasskey) { - return ( - - ); - } - - if (hasPasskeyAutofillProp && !allowedTypeForPasskey) { - logger.warnOnce( - ` can only be used with or `, - ); - } else if (hasPasskeyAutofillProp) { - logger.warnOnce( - ` can only be used inside in order to trigger a sign-in attempt, otherwise it will be ignored.`, - ); - } - - return ( - - ); - }, -); - -Input.displayName = DISPLAY_NAME; - -const InputWithPasskeyAutofill = React.forwardRef, FormInputProps>( - (props: FormInputProps, forwardedRef) => { - const signInRouterRef = SignInRouterCtx.useActorRef(true); - const passkeyAutofillSupported = useSignInPasskeyAutofill(); - - React.useEffect(() => { - if (passkeyAutofillSupported) { - signInRouterRef?.send({ type: 'AUTHENTICATE.PASSKEY.AUTOFILL' }); - } - }, [passkeyAutofillSupported, signInRouterRef]); - - const field = useInput(props); - return ( - - ); - }, -); - -InputWithPasskeyAutofill.displayName = DISPLAY_NAME_PASSKEYS_AUTOFILL; diff --git a/packages/elements/src/react/common/form/label.tsx b/packages/elements/src/react/common/form/label.tsx deleted file mode 100644 index 565ead07a4e..00000000000 --- a/packages/elements/src/react/common/form/label.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Label as RadixLabel } from '@radix-ui/react-form'; - -const DISPLAY_NAME = 'ClerkElementsLabel'; - -/** - * Renders a `