diff --git a/.changeset/old-peaches-share.md b/.changeset/old-peaches-share.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/old-peaches-share.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/rspack.config.js b/packages/clerk-js/rspack.config.js index 31dcf9c2fe..28abc4eedf 100644 --- a/packages/clerk-js/rspack.config.js +++ b/packages/clerk-js/rspack.config.js @@ -323,7 +323,7 @@ const prodConfig = ({ mode, env, analysis }) => { entryForVariant(variants.clerkBrowser), isSandbox ? { - entry: { sandbox: './sandbox/app.js' }, + entry: { sandbox: './sandbox/app.ts' }, plugins: [ new rspack.HtmlRspackPlugin({ minify: false, @@ -530,7 +530,7 @@ const devConfig = ({ mode, env }) => { // prettier-ignore [variants.clerkBrowser]: merge( entryForVariant(variants.clerkBrowser), - isSandbox ? { entry: { sandbox: './sandbox/app.js' } } : {}, + isSandbox ? { entry: { sandbox: './sandbox/app.ts' } } : {}, common({ mode }), commonForDev(), ), diff --git a/packages/clerk-js/sandbox/app.js b/packages/clerk-js/sandbox/app.js deleted file mode 100644 index acf9bfbe62..0000000000 --- a/packages/clerk-js/sandbox/app.js +++ /dev/null @@ -1,189 +0,0 @@ -//@ts-check - -/** @typedef {import('@clerk/clerk-js').Clerk} Clerk */ - -/** - * @typedef {object} ComponentPropsControl - * @property {(props: unknown) => void} setProps - * @property {() => (any | null)} getProps - */ - -const AVAILABLE_COMPONENTS = /** @type {const} */ ([ - 'clerk', // While not a component, we want to support passing options to the Clerk class. - 'signIn', - 'signUp', - 'userButton', - 'userProfile', - 'createOrganization', - 'organizationList', - 'organizationProfile', - 'organizationSwitcher', - 'waitlist', -]); - -const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox'; - -const urlParams = new URL(window.location.href).searchParams; -for (const [component, encodedProps] of urlParams.entries()) { - if (AVAILABLE_COMPONENTS.includes(/** @type {typeof AVAILABLE_COMPONENTS[number]} */ (component))) { - localStorage.setItem(`${COMPONENT_PROPS_NAMESPACE}-${component}`, encodedProps); - } -} - -/** - * @param {typeof AVAILABLE_COMPONENTS[number]} component - * @param {unknown} props - */ -function setComponentProps(component, props) { - const encodedProps = JSON.stringify(props); - - const url = new URL(window.location.href); - url.searchParams.set(component, encodedProps); - - window.location.href = url.toString(); -} - -/** - * @param {typeof AVAILABLE_COMPONENTS[number]} component - * @returns {unknown | null} - */ -function getComponentProps(component) { - const url = new URL(window.location.href); - const encodedProps = url.searchParams.get(component); - if (encodedProps) { - return JSON.parse(encodedProps); - } - - const localEncodedProps = localStorage.getItem(`${COMPONENT_PROPS_NAMESPACE}-${component}`); - if (localEncodedProps) { - return JSON.parse(localEncodedProps); - } - - return null; -} - -/** - * @param {typeof AVAILABLE_COMPONENTS[number]} component - * @returns {ComponentPropsControl} - */ -function buildComponentControls(component) { - return { - setProps(props) { - setComponentProps(component, props); - }, - getProps() { - return getComponentProps(component); - }, - }; -} - -/** - * @type {Record} - */ -const componentControls = { - clerk: buildComponentControls('clerk'), - signIn: buildComponentControls('signIn'), - signUp: buildComponentControls('signUp'), - userButton: buildComponentControls('userButton'), - userProfile: buildComponentControls('userProfile'), - createOrganization: buildComponentControls('createOrganization'), - organizationList: buildComponentControls('organizationList'), - organizationProfile: buildComponentControls('organizationProfile'), - organizationSwitcher: buildComponentControls('organizationSwitcher'), - waitlist: buildComponentControls('waitlist'), -}; - -/** - * @typedef {object} CustomWindowObject - * @property {Clerk} [Clerk] - * @property {Record} [components] - */ - -/** - * @typedef {Window & CustomWindowObject} CustomWindow - */ - -/** @type {CustomWindow} */ -const windowWithClerk = window; -windowWithClerk.components = componentControls; - -const Clerk = /** @type {Clerk} **/ (windowWithClerk.Clerk); -if (!Clerk) { - throw new Error(`clerk-js is not loaded!`); -} - -const app = /** @type {HTMLDivElement} **/ (document.getElementById('app')); - -/** - * @param {HTMLDivElement} element - */ -function mountIndex(element) { - const user = Clerk.user; - element.innerHTML = `
${JSON.stringify({ user }, null, 2)}
`; -} - -/** @typedef {keyof typeof routes} Route */ - -const routes = { - '/': () => { - mountIndex(app); - }, - '/sign-in': () => { - Clerk.mountSignIn(app, componentControls.signIn.getProps() ?? {}); - }, - '/sign-up': () => { - Clerk.mountSignUp(app, componentControls.signUp.getProps() ?? {}); - }, - '/user-button': () => { - Clerk.mountUserButton(app, componentControls.userButton.getProps() ?? {}); - }, - '/user-profile': () => { - Clerk.mountUserProfile(app, componentControls.userProfile.getProps() ?? {}); - }, - '/create-organization': () => { - Clerk.mountCreateOrganization(app, componentControls.createOrganization.getProps() ?? {}); - }, - '/organization-list': () => { - Clerk.mountOrganizationList(app, componentControls.organizationList.getProps() ?? {}); - }, - '/organization-profile': () => { - Clerk.mountOrganizationProfile(app, componentControls.organizationProfile.getProps() ?? {}); - }, - '/organization-switcher': () => { - Clerk.mountOrganizationSwitcher(app, componentControls.organizationSwitcher.getProps() ?? {}); - }, - '/waitlist': () => { - Clerk.mountWaitlist(app, componentControls.waitlist.getProps() ?? {}); - }, - '/accountless': () => { - Clerk.__unstable__updateProps({ options: { __internal_claimAccountlessKeysUrl: '/test-url' } }); - }, -}; - -/** - * @param {string} currentRoute - */ -function addCurrentRouteIndicator(currentRoute) { - const link = document.querySelector(`a[href="${currentRoute}"]`); - if (!link) { - return; - } - link.removeAttribute('aria-current'); - link.setAttribute('aria-current', 'page'); -} - -(async () => { - const route = window.location.pathname; - if (route in routes) { - const renderCurrentRoute = routes[route]; - addCurrentRouteIndicator(route); - await Clerk.load({ - ...(componentControls.clerk.getProps() ?? {}), - signInUrl: '/sign-in', - signUpUrl: '/sign-up', - }); - renderCurrentRoute(); - } else { - console.error(`Unknown route: "${route}".`); - } -})(); diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts new file mode 100644 index 0000000000..73946bf476 --- /dev/null +++ b/packages/clerk-js/sandbox/app.ts @@ -0,0 +1,162 @@ +import type { Clerk as ClerkType } from '../'; + +interface ComponentPropsControl { + setProps: (props: unknown) => void; + getProps: () => any | null; +} + +const AVAILABLE_COMPONENTS = [ + 'clerk', // While not a component, we want to support passing options to the Clerk class. + 'signIn', + 'signUp', + 'userButton', + 'userProfile', + 'createOrganization', + 'organizationList', + 'organizationProfile', + 'organizationSwitcher', + 'waitlist', +] as const; + +const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox'; + +const urlParams = new URL(window.location.href).searchParams; +for (const [component, encodedProps] of urlParams.entries()) { + if (AVAILABLE_COMPONENTS.includes(component as (typeof AVAILABLE_COMPONENTS)[number])) { + localStorage.setItem(`${COMPONENT_PROPS_NAMESPACE}-${component}`, encodedProps); + } +} + +function setComponentProps(component: (typeof AVAILABLE_COMPONENTS)[number], props: unknown) { + const encodedProps = JSON.stringify(props); + + const url = new URL(window.location.href); + url.searchParams.set(component, encodedProps); + + window.location.href = url.toString(); +} + +function getComponentProps(component: (typeof AVAILABLE_COMPONENTS)[number]): unknown | null { + const url = new URL(window.location.href); + const encodedProps = url.searchParams.get(component); + if (encodedProps) { + return JSON.parse(encodedProps); + } + + const localEncodedProps = localStorage.getItem(`${COMPONENT_PROPS_NAMESPACE}-${component}`); + if (localEncodedProps) { + return JSON.parse(localEncodedProps); + } + + return null; +} + +function buildComponentControls(component: (typeof AVAILABLE_COMPONENTS)[number]): ComponentPropsControl { + return { + setProps(props) { + setComponentProps(component, props); + }, + getProps() { + return getComponentProps(component); + }, + }; +} + +const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], ComponentPropsControl> = { + clerk: buildComponentControls('clerk'), + signIn: buildComponentControls('signIn'), + signUp: buildComponentControls('signUp'), + userButton: buildComponentControls('userButton'), + userProfile: buildComponentControls('userProfile'), + createOrganization: buildComponentControls('createOrganization'), + organizationList: buildComponentControls('organizationList'), + organizationProfile: buildComponentControls('organizationProfile'), + organizationSwitcher: buildComponentControls('organizationSwitcher'), + waitlist: buildComponentControls('waitlist'), +}; + +declare global { + interface Window { + components: Record<(typeof AVAILABLE_COMPONENTS)[number], ComponentPropsControl>; + } +} + +window.components = componentControls; + +const Clerk = window.Clerk; +function assertClerkIsLoaded(c: ClerkType | undefined): asserts c is ClerkType { + if (!c) { + throw new Error('Clerk is not loaded'); + } +} + +const app = document.getElementById('app') as HTMLDivElement; + +function mountIndex(element: HTMLDivElement) { + assertClerkIsLoaded(Clerk); + const user = Clerk.user; + element.innerHTML = `
${JSON.stringify({ user }, null, 2)}
`; +} + +function addCurrentRouteIndicator(currentRoute: string) { + const link = document.querySelector(`a[href="${currentRoute}"]`); + if (!link) { + return; + } + link.removeAttribute('aria-current'); + link.setAttribute('aria-current', 'page'); +} + +(async () => { + assertClerkIsLoaded(Clerk); + + const routes = { + '/': () => { + mountIndex(app); + }, + '/sign-in': () => { + Clerk.mountSignIn(app, componentControls.signIn.getProps() ?? {}); + }, + '/sign-up': () => { + Clerk.mountSignUp(app, componentControls.signUp.getProps() ?? {}); + }, + '/user-button': () => { + Clerk.mountUserButton(app, componentControls.userButton.getProps() ?? {}); + }, + '/user-profile': () => { + Clerk.mountUserProfile(app, componentControls.userProfile.getProps() ?? {}); + }, + '/create-organization': () => { + Clerk.mountCreateOrganization(app, componentControls.createOrganization.getProps() ?? {}); + }, + '/organization-list': () => { + Clerk.mountOrganizationList(app, componentControls.organizationList.getProps() ?? {}); + }, + '/organization-profile': () => { + Clerk.mountOrganizationProfile(app, componentControls.organizationProfile.getProps() ?? {}); + }, + '/organization-switcher': () => { + Clerk.mountOrganizationSwitcher(app, componentControls.organizationSwitcher.getProps() ?? {}); + }, + '/waitlist': () => { + Clerk.mountWaitlist(app, componentControls.waitlist.getProps() ?? {}); + }, + '/accountless': () => { + Clerk.__unstable__updateProps({ options: { __internal_claimAccountlessKeysUrl: '/test-url' } }); + }, + }; + + const route = window.location.pathname; + if (route in routes) { + const renderCurrentRoute = routes[route]; + addCurrentRouteIndicator(route); + await Clerk.load({ + ...(componentControls.clerk.getProps() ?? {}), + signInUrl: '/sign-in', + signUpUrl: '/sign-up', + }); + renderCurrentRoute(); + } else { + console.error(`Unknown route: "${route}".`); + } +})(); diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html index fe78ed6bff..eee3b5867e 100644 --- a/packages/clerk-js/sandbox/template.html +++ b/packages/clerk-js/sandbox/template.html @@ -133,7 +133,7 @@ viewBox="0 0 62 18" fill="none" aria-hidden="true" - class="h-[1.125rem] text-gray-950 dark:text-white" + class="h-[1.125rem] text-gray-950" >