Skip to content

Commit

Permalink
feat(clerk-js): Introduce internal Accountless UI prompt in sandbox (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef authored Nov 21, 2024
1 parent 3c21cd6 commit c70994b
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-bats-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/types': patch
---

Add `__internal_claimAccountlessKeysUrl` to `ClerkOptions`.
5 changes: 5 additions & 0 deletions .changeset/empty-fans-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Add new internal UI component for accountless.
3 changes: 3 additions & 0 deletions packages/clerk-js/sandbox/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ const routes = {
'/waitlist': () => {
Clerk.mountWaitlist(app, componentControls.waitlist.getProps() ?? {});
},
'/accountless': () => {
Clerk.__unstable__updateProps({ options: { __internal_claimAccountlessKeysUrl: '/test-url' } });
},
};

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/clerk-js/sandbox/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@
>Waitlist</a
>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/accountless"
>
Accountless
</a>
</li>
</ul>
</nav>
</div>
Expand Down
11 changes: 11 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,7 @@ export class Clerk implements ClerkInterface {
this.#clearClerkQueryParams();

this.#handleImpersonationFab();
this.#handleAccountlessPrompt();
return true;
};

Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -516,6 +517,12 @@ const Components = (props: ComponentsProps) => {
</LazyImpersonationFabProvider>
)}

{state.options?.__internal_claimAccountlessKeysUrl && (
<LazyImpersonationFabProvider globalAppearance={state.appearance}>
<AccountlessPrompt url={state.options.__internal_claimAccountlessKeysUrl} />
</LazyImpersonationFabProvider>
)}

<Suspense>{state.organizationSwitcherPrefetch && <OrganizationSwitcherPrefetch />}</Suspense>
</LazyProviders>
</Suspense>
Expand Down
176 changes: 176 additions & 0 deletions packages/clerk-js/src/ui/components/AccountlessPrompt/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Col
sx={t => ({
width: '100%',
paddingLeft: t.sizes.$4,
paddingRight: t.sizes.$6,
whiteSpace: 'nowrap',
})}
>
<Text
colorScheme='secondary'
elementDescriptor={descriptors.impersonationFabTitle}
variant='buttonLarge'
truncate
localizationKey={title}
/>
<Link
variant='buttonLarge'
elementDescriptor={descriptors.impersonationFabActionLink}
sx={t => ({
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!)
}
/>
</Col>
);
};

export const _AccountlessPrompt = (props: AccountlessPromptProps) => {
// const { parsedInternalTheme } = useAppearance();
const containerRef = useRef<HTMLDivElement>(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 (
<Portal>
<Flex
ref={containerRef}
elementDescriptor={descriptors.impersonationFab}
onPointerDown={onPointerDown}
align='center'
sx={t => ({
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
<Flex
id='cl-impersonationText'
sx={t => ({
transition: `max-width ${t.transitionDuration.$slowest} ease, opacity ${t.transitionDuration.$fast} ease`,
maxWidth: '0px',
opacity: 1,
})}
>
<FabContent
url={props.url}
signOutText={'Claim your keys'}
/>
</Flex>
</Flex>
</Portal>
);
};

export const AccountlessPrompt = (props: AccountlessPromptProps) => (
<InternalThemeProvider>
<_AccountlessPrompt {...props} />
</InternalThemeProvider>
);
4 changes: 4 additions & 0 deletions packages/clerk-js/src/ui/lazyModules/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })));
Expand Down Expand Up @@ -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]?.();
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,8 @@ export type ClerkOptions = ClerkOptionsNavigation &
Record<string, any>
>;

__internal_claimAccountlessKeysUrl?: string;

/**
* [EXPERIMENTAL] Provide the underlying host router, required for the new experimental UI components.
*/
Expand Down

0 comments on commit c70994b

Please sign in to comment.