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. */