diff --git a/.changeset/clever-bats-own.md b/.changeset/clever-bats-own.md
new file mode 100644
index 0000000000..bd2a121438
--- /dev/null
+++ b/.changeset/clever-bats-own.md
@@ -0,0 +1,5 @@
+---
+'@clerk/types': patch
+---
+
+Add `__internal_claimAccountlessKeysUrl` to `ClerkOptions`.
diff --git a/.changeset/empty-fans-attack.md b/.changeset/empty-fans-attack.md
new file mode 100644
index 0000000000..e3b32213db
--- /dev/null
+++ b/.changeset/empty-fans-attack.md
@@ -0,0 +1,5 @@
+---
+'@clerk/clerk-js': patch
+---
+
+Add new internal UI component for accountless.
diff --git a/packages/clerk-js/sandbox/app.js b/packages/clerk-js/sandbox/app.js
index 6f7debdec9..a2602ca4e3 100644
--- a/packages/clerk-js/sandbox/app.js
+++ b/packages/clerk-js/sandbox/app.js
@@ -153,6 +153,9 @@ const routes = {
'/waitlist': () => {
Clerk.mountWaitlist(app, componentControls.waitlist.getProps() ?? {});
},
+ '/accountless': () => {
+ Clerk.__unstable__updateProps({ options: { __internal_claimAccountlessKeysUrl: '/test-url' } });
+ },
};
/**
diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html
index 3d1c1eea4c..fe78ed6bff 100644
--- a/packages/clerk-js/sandbox/template.html
+++ b/packages/clerk-js/sandbox/template.html
@@ -236,6 +236,14 @@
>Waitlist
+
+
+ Accountless
+
+
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index db377ee7cc..78b322a258 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -1831,6 +1831,7 @@ export class Clerk implements ClerkInterface {
this.#clearClerkQueryParams();
this.#handleImpersonationFab();
+ this.#handleAccountlessPrompt();
return true;
};
@@ -1960,6 +1961,16 @@ export class Clerk implements ClerkInterface {
});
};
+ #handleAccountlessPrompt = () => {
+ void this.#componentControls?.ensureMounted().then(controls => {
+ if (this.#options.__internal_claimAccountlessKeysUrl) {
+ controls.updateProps({
+ options: { __internal_claimAccountlessKeysUrl: this.#options.__internal_claimAccountlessKeysUrl },
+ });
+ }
+ });
+ };
+
#buildUrl = (
key: 'signInUrl' | 'signUpUrl',
options: RedirectOptions,
diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx
index 813800f4aa..f12eac988c 100644
--- a/packages/clerk-js/src/ui/Components.tsx
+++ b/packages/clerk-js/src/ui/Components.tsx
@@ -23,6 +23,7 @@ import type { AppearanceCascade } from './customizables/parseAppearance';
import { useClerkModalStateParams } from './hooks/useClerkModalStateParams';
import type { ClerkComponentName } from './lazyModules/components';
import {
+ AccountlessPrompt,
BlankCaptchaModal,
CreateOrganizationModal,
ImpersonationFab,
@@ -516,6 +517,12 @@ const Components = (props: ComponentsProps) => {
)}
+ {state.options?.__internal_claimAccountlessKeysUrl && (
+
+
+
+ )}
+
{state.organizationSwitcherPrefetch && }
diff --git a/packages/clerk-js/src/ui/components/AccountlessPrompt/index.tsx b/packages/clerk-js/src/ui/components/AccountlessPrompt/index.tsx
new file mode 100644
index 0000000000..413030fb60
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/AccountlessPrompt/index.tsx
@@ -0,0 +1,176 @@
+import type { PointerEventHandler } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
+
+import type { LocalizationKey } from '../../customizables';
+import { Col, descriptors, Flex, Link, Text } from '../../customizables';
+import { Portal } from '../../elements/Portal';
+import { InternalThemeProvider, mqu } from '../../styledSystem';
+
+type AccountlessPromptProps = {
+ url?: string;
+};
+
+type FabContentProps = { title?: LocalizationKey | string; signOutText: LocalizationKey | string; url: string };
+
+const FabContent = ({ title, signOutText, url }: FabContentProps) => {
+ return (
+ ({
+ width: '100%',
+ paddingLeft: t.sizes.$4,
+ paddingRight: t.sizes.$6,
+ whiteSpace: 'nowrap',
+ })}
+ >
+
+ ({
+ alignSelf: 'flex-start',
+ color: t.colors.$primary500,
+ ':hover': {
+ cursor: 'pointer',
+ },
+ })}
+ localizationKey={signOutText}
+ onClick={
+ () => (window.location.href = url)
+ // clerk-js has been loaded at this point so we can safely access session
+ // handleSignOutSessionClicked(session!)
+ }
+ />
+
+ );
+};
+
+export const _AccountlessPrompt = (props: AccountlessPromptProps) => {
+ // const { parsedInternalTheme } = useAppearance();
+ const containerRef = useRef(null);
+
+ //essentials for calcs
+ // const eyeWidth = parsedInternalTheme.sizes.$16;
+ // const eyeHeight = eyeWidth;
+ const topProperty = '--cl-impersonation-fab-top';
+ const rightProperty = '--cl-impersonation-fab-right';
+ const defaultTop = 109;
+ const defaultRight = 23;
+
+ const handleResize = () => {
+ const current = containerRef.current;
+ if (!current) {
+ return;
+ }
+
+ const offsetRight = window.innerWidth - current.offsetLeft - current.offsetWidth;
+ const offsetBottom = window.innerHeight - current.offsetTop - current.offsetHeight;
+
+ const outsideViewport = [current.offsetLeft, offsetRight, current.offsetTop, offsetBottom].some(o => o < 0);
+
+ if (outsideViewport) {
+ document.documentElement.style.setProperty(rightProperty, `${defaultRight}px`);
+ document.documentElement.style.setProperty(topProperty, `${defaultTop}px`);
+ }
+ };
+
+ const onPointerDown: PointerEventHandler = () => {
+ window.addEventListener('pointermove', onPointerMove);
+ window.addEventListener(
+ 'pointerup',
+ () => {
+ window.removeEventListener('pointermove', onPointerMove);
+ handleResize();
+ },
+ { once: true },
+ );
+ };
+
+ const onPointerMove = useCallback((e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const current = containerRef.current;
+ if (!current) {
+ return;
+ }
+ const rightOffestBasedOnViewportAndContent = `${
+ window.innerWidth - current.offsetLeft - current.offsetWidth - e.movementX
+ }px`;
+ document.documentElement.style.setProperty(rightProperty, rightOffestBasedOnViewportAndContent);
+ document.documentElement.style.setProperty(topProperty, `${current.offsetTop - -e.movementY}px`);
+ }, []);
+
+ const repositionFabOnResize = () => {
+ window.addEventListener('resize', handleResize);
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ };
+ };
+
+ useEffect(repositionFabOnResize, []);
+
+ if (!props.url) {
+ return null;
+ }
+
+ return (
+
+ ({
+ touchAction: 'none', //for drag to work on mobile consistently
+ position: 'fixed',
+ overflow: 'hidden',
+ top: `var(${topProperty}, ${defaultTop}px)`,
+ right: `var(${rightProperty}, ${defaultRight}px)`,
+ padding: `10px`,
+ zIndex: t.zIndices.$fab,
+ boxShadow: t.shadows.$fabShadow,
+ borderRadius: t.radii.$halfHeight, //to match the circular eye perfectly
+ backgroundColor: t.colors.$white,
+ fontFamily: t.fonts.$main,
+ ':hover': {
+ cursor: 'grab',
+ },
+ ':hover #cl-impersonationText': {
+ transition: `max-width ${t.transitionDuration.$slowest} ease, opacity 0ms ease ${t.transitionDuration.$slowest}`,
+ maxWidth: `min(calc(50vw - 2 * ${defaultRight}px), ${15}ch)`,
+ [mqu.md]: {
+ maxWidth: `min(calc(100vw - 2 * ${defaultRight}px), ${15}ch)`,
+ },
+ opacity: 1,
+ },
+ })}
+ >
+ 🔓Accountless Mode
+ ({
+ transition: `max-width ${t.transitionDuration.$slowest} ease, opacity ${t.transitionDuration.$fast} ease`,
+ maxWidth: '0px',
+ opacity: 1,
+ })}
+ >
+
+
+
+
+ );
+};
+
+export const AccountlessPrompt = (props: AccountlessPromptProps) => (
+
+ <_AccountlessPrompt {...props} />
+
+);
diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts
index bbcd5ee1a4..8aae06059d 100644
--- a/packages/clerk-js/src/ui/lazyModules/components.ts
+++ b/packages/clerk-js/src/ui/lazyModules/components.ts
@@ -16,6 +16,7 @@ const componentImportPaths = {
BlankCaptchaModal: () => import(/* webpackChunkName: "blankcaptcha" */ './../components/BlankCaptchaModal'),
UserVerification: () => import(/* webpackChunkName: "userverification" */ './../components/UserVerification'),
Waitlist: () => import(/* webpackChunkName: "waitlist" */ './../components/Waitlist'),
+ AccountlessPrompt: () => import(/* webpackChunkName: "accountlessPrompt" */ './../components/AccountlessPrompt'),
} as const;
export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn })));
@@ -83,6 +84,9 @@ export const BlankCaptchaModal = lazy(() =>
export const ImpersonationFab = lazy(() =>
componentImportPaths.ImpersonationFab().then(module => ({ default: module.ImpersonationFab })),
);
+export const AccountlessPrompt = lazy(() =>
+ componentImportPaths.AccountlessPrompt().then(module => ({ default: module.AccountlessPrompt })),
+);
export const preloadComponent = async (component: unknown) => {
return componentImportPaths[component as keyof typeof componentImportPaths]?.();
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 48d89b411d..3450c49b22 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -745,6 +745,8 @@ export type ClerkOptions = ClerkOptionsNavigation &
Record
>;
+ __internal_claimAccountlessKeysUrl?: string;
+
/**
* [EXPERIMENTAL] Provide the underlying host router, required for the new experimental UI components.
*/