Skip to content

Commit c70994b

Browse files
authoredNov 21, 2024··
feat(clerk-js): Introduce internal Accountless UI prompt in sandbox (#4625)
1 parent 3c21cd6 commit c70994b

File tree

9 files changed

+221
-0
lines changed

9 files changed

+221
-0
lines changed
 

‎.changeset/clever-bats-own.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/types': patch
3+
---
4+
5+
Add `__internal_claimAccountlessKeysUrl` to `ClerkOptions`.

‎.changeset/empty-fans-attack.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Add new internal UI component for accountless.

‎packages/clerk-js/sandbox/app.js

+3
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ const routes = {
153153
'/waitlist': () => {
154154
Clerk.mountWaitlist(app, componentControls.waitlist.getProps() ?? {});
155155
},
156+
'/accountless': () => {
157+
Clerk.__unstable__updateProps({ options: { __internal_claimAccountlessKeysUrl: '/test-url' } });
158+
},
156159
};
157160

158161
/**

‎packages/clerk-js/sandbox/template.html

+8
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@
236236
>Waitlist</a
237237
>
238238
</li>
239+
<li class="relative">
240+
<a
241+
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"
242+
href="/accountless"
243+
>
244+
Accountless
245+
</a>
246+
</li>
239247
</ul>
240248
</nav>
241249
</div>

‎packages/clerk-js/src/core/clerk.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1831,6 +1831,7 @@ export class Clerk implements ClerkInterface {
18311831
this.#clearClerkQueryParams();
18321832

18331833
this.#handleImpersonationFab();
1834+
this.#handleAccountlessPrompt();
18341835
return true;
18351836
};
18361837

@@ -1960,6 +1961,16 @@ export class Clerk implements ClerkInterface {
19601961
});
19611962
};
19621963

1964+
#handleAccountlessPrompt = () => {
1965+
void this.#componentControls?.ensureMounted().then(controls => {
1966+
if (this.#options.__internal_claimAccountlessKeysUrl) {
1967+
controls.updateProps({
1968+
options: { __internal_claimAccountlessKeysUrl: this.#options.__internal_claimAccountlessKeysUrl },
1969+
});
1970+
}
1971+
});
1972+
};
1973+
19631974
#buildUrl = (
19641975
key: 'signInUrl' | 'signUpUrl',
19651976
options: RedirectOptions,

‎packages/clerk-js/src/ui/Components.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { AppearanceCascade } from './customizables/parseAppearance';
2323
import { useClerkModalStateParams } from './hooks/useClerkModalStateParams';
2424
import type { ClerkComponentName } from './lazyModules/components';
2525
import {
26+
AccountlessPrompt,
2627
BlankCaptchaModal,
2728
CreateOrganizationModal,
2829
ImpersonationFab,
@@ -516,6 +517,12 @@ const Components = (props: ComponentsProps) => {
516517
</LazyImpersonationFabProvider>
517518
)}
518519

520+
{state.options?.__internal_claimAccountlessKeysUrl && (
521+
<LazyImpersonationFabProvider globalAppearance={state.appearance}>
522+
<AccountlessPrompt url={state.options.__internal_claimAccountlessKeysUrl} />
523+
</LazyImpersonationFabProvider>
524+
)}
525+
519526
<Suspense>{state.organizationSwitcherPrefetch && <OrganizationSwitcherPrefetch />}</Suspense>
520527
</LazyProviders>
521528
</Suspense>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import type { PointerEventHandler } from 'react';
2+
import { useCallback, useEffect, useRef } from 'react';
3+
4+
import type { LocalizationKey } from '../../customizables';
5+
import { Col, descriptors, Flex, Link, Text } from '../../customizables';
6+
import { Portal } from '../../elements/Portal';
7+
import { InternalThemeProvider, mqu } from '../../styledSystem';
8+
9+
type AccountlessPromptProps = {
10+
url?: string;
11+
};
12+
13+
type FabContentProps = { title?: LocalizationKey | string; signOutText: LocalizationKey | string; url: string };
14+
15+
const FabContent = ({ title, signOutText, url }: FabContentProps) => {
16+
return (
17+
<Col
18+
sx={t => ({
19+
width: '100%',
20+
paddingLeft: t.sizes.$4,
21+
paddingRight: t.sizes.$6,
22+
whiteSpace: 'nowrap',
23+
})}
24+
>
25+
<Text
26+
colorScheme='secondary'
27+
elementDescriptor={descriptors.impersonationFabTitle}
28+
variant='buttonLarge'
29+
truncate
30+
localizationKey={title}
31+
/>
32+
<Link
33+
variant='buttonLarge'
34+
elementDescriptor={descriptors.impersonationFabActionLink}
35+
sx={t => ({
36+
alignSelf: 'flex-start',
37+
color: t.colors.$primary500,
38+
':hover': {
39+
cursor: 'pointer',
40+
},
41+
})}
42+
localizationKey={signOutText}
43+
onClick={
44+
() => (window.location.href = url)
45+
// clerk-js has been loaded at this point so we can safely access session
46+
// handleSignOutSessionClicked(session!)
47+
}
48+
/>
49+
</Col>
50+
);
51+
};
52+
53+
export const _AccountlessPrompt = (props: AccountlessPromptProps) => {
54+
// const { parsedInternalTheme } = useAppearance();
55+
const containerRef = useRef<HTMLDivElement>(null);
56+
57+
//essentials for calcs
58+
// const eyeWidth = parsedInternalTheme.sizes.$16;
59+
// const eyeHeight = eyeWidth;
60+
const topProperty = '--cl-impersonation-fab-top';
61+
const rightProperty = '--cl-impersonation-fab-right';
62+
const defaultTop = 109;
63+
const defaultRight = 23;
64+
65+
const handleResize = () => {
66+
const current = containerRef.current;
67+
if (!current) {
68+
return;
69+
}
70+
71+
const offsetRight = window.innerWidth - current.offsetLeft - current.offsetWidth;
72+
const offsetBottom = window.innerHeight - current.offsetTop - current.offsetHeight;
73+
74+
const outsideViewport = [current.offsetLeft, offsetRight, current.offsetTop, offsetBottom].some(o => o < 0);
75+
76+
if (outsideViewport) {
77+
document.documentElement.style.setProperty(rightProperty, `${defaultRight}px`);
78+
document.documentElement.style.setProperty(topProperty, `${defaultTop}px`);
79+
}
80+
};
81+
82+
const onPointerDown: PointerEventHandler = () => {
83+
window.addEventListener('pointermove', onPointerMove);
84+
window.addEventListener(
85+
'pointerup',
86+
() => {
87+
window.removeEventListener('pointermove', onPointerMove);
88+
handleResize();
89+
},
90+
{ once: true },
91+
);
92+
};
93+
94+
const onPointerMove = useCallback((e: PointerEvent) => {
95+
e.stopPropagation();
96+
e.preventDefault();
97+
const current = containerRef.current;
98+
if (!current) {
99+
return;
100+
}
101+
const rightOffestBasedOnViewportAndContent = `${
102+
window.innerWidth - current.offsetLeft - current.offsetWidth - e.movementX
103+
}px`;
104+
document.documentElement.style.setProperty(rightProperty, rightOffestBasedOnViewportAndContent);
105+
document.documentElement.style.setProperty(topProperty, `${current.offsetTop - -e.movementY}px`);
106+
}, []);
107+
108+
const repositionFabOnResize = () => {
109+
window.addEventListener('resize', handleResize);
110+
return () => {
111+
window.removeEventListener('resize', handleResize);
112+
};
113+
};
114+
115+
useEffect(repositionFabOnResize, []);
116+
117+
if (!props.url) {
118+
return null;
119+
}
120+
121+
return (
122+
<Portal>
123+
<Flex
124+
ref={containerRef}
125+
elementDescriptor={descriptors.impersonationFab}
126+
onPointerDown={onPointerDown}
127+
align='center'
128+
sx={t => ({
129+
touchAction: 'none', //for drag to work on mobile consistently
130+
position: 'fixed',
131+
overflow: 'hidden',
132+
top: `var(${topProperty}, ${defaultTop}px)`,
133+
right: `var(${rightProperty}, ${defaultRight}px)`,
134+
padding: `10px`,
135+
zIndex: t.zIndices.$fab,
136+
boxShadow: t.shadows.$fabShadow,
137+
borderRadius: t.radii.$halfHeight, //to match the circular eye perfectly
138+
backgroundColor: t.colors.$white,
139+
fontFamily: t.fonts.$main,
140+
':hover': {
141+
cursor: 'grab',
142+
},
143+
':hover #cl-impersonationText': {
144+
transition: `max-width ${t.transitionDuration.$slowest} ease, opacity 0ms ease ${t.transitionDuration.$slowest}`,
145+
maxWidth: `min(calc(50vw - 2 * ${defaultRight}px), ${15}ch)`,
146+
[mqu.md]: {
147+
maxWidth: `min(calc(100vw - 2 * ${defaultRight}px), ${15}ch)`,
148+
},
149+
opacity: 1,
150+
},
151+
})}
152+
>
153+
🔓Accountless Mode
154+
<Flex
155+
id='cl-impersonationText'
156+
sx={t => ({
157+
transition: `max-width ${t.transitionDuration.$slowest} ease, opacity ${t.transitionDuration.$fast} ease`,
158+
maxWidth: '0px',
159+
opacity: 1,
160+
})}
161+
>
162+
<FabContent
163+
url={props.url}
164+
signOutText={'Claim your keys'}
165+
/>
166+
</Flex>
167+
</Flex>
168+
</Portal>
169+
);
170+
};
171+
172+
export const AccountlessPrompt = (props: AccountlessPromptProps) => (
173+
<InternalThemeProvider>
174+
<_AccountlessPrompt {...props} />
175+
</InternalThemeProvider>
176+
);

‎packages/clerk-js/src/ui/lazyModules/components.ts

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const componentImportPaths = {
1616
BlankCaptchaModal: () => import(/* webpackChunkName: "blankcaptcha" */ './../components/BlankCaptchaModal'),
1717
UserVerification: () => import(/* webpackChunkName: "userverification" */ './../components/UserVerification'),
1818
Waitlist: () => import(/* webpackChunkName: "waitlist" */ './../components/Waitlist'),
19+
AccountlessPrompt: () => import(/* webpackChunkName: "accountlessPrompt" */ './../components/AccountlessPrompt'),
1920
} as const;
2021

2122
export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn })));
@@ -83,6 +84,9 @@ export const BlankCaptchaModal = lazy(() =>
8384
export const ImpersonationFab = lazy(() =>
8485
componentImportPaths.ImpersonationFab().then(module => ({ default: module.ImpersonationFab })),
8586
);
87+
export const AccountlessPrompt = lazy(() =>
88+
componentImportPaths.AccountlessPrompt().then(module => ({ default: module.AccountlessPrompt })),
89+
);
8690

8791
export const preloadComponent = async (component: unknown) => {
8892
return componentImportPaths[component as keyof typeof componentImportPaths]?.();

‎packages/types/src/clerk.ts

+2
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,8 @@ export type ClerkOptions = ClerkOptionsNavigation &
745745
Record<string, any>
746746
>;
747747

748+
__internal_claimAccountlessKeysUrl?: string;
749+
748750
/**
749751
* [EXPERIMENTAL] Provide the underlying host router, required for the new experimental UI components.
750752
*/

0 commit comments

Comments
 (0)
Please sign in to comment.