From cde94dfa6458e89e1e9efb8015aaf5f03c2cb062 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 6 Feb 2026 09:49:29 -0500 Subject: [PATCH 1/7] chore(clerk-js): Keyless prompt content updates --- .../devPrompts/KeylessPrompt/index.tsx | 97 +++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx b/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx index 94ea4cbd287..dc73ac95c5a 100644 --- a/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx +++ b/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx @@ -41,7 +41,7 @@ function withLastActiveFallback(cb: () => string): string { const KeylessPromptInternal = (_props: KeylessPromptProps) => { const { isSignedIn } = useUser(); - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(true); useEffect(() => { if (isSignedIn) { @@ -83,7 +83,6 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { justify-content: center; width: 100%; height: 1.75rem; - max-width: 14.625rem; padding: 0.25rem 0.625rem; border-radius: 0.375rem; font-size: 0.75rem; @@ -112,7 +111,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { bottom: '1.25rem', right: '1.25rem', height: `${t.sizes.$10}`, - minWidth: '13.4rem', + minWidth: '15.5rem', paddingLeft: `${t.space.$3}`, borderRadius: '1.25rem', transition: 'all 195ms cubic-bezier(0.2, 0.61, 0.1, 1)', @@ -125,10 +124,10 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { flexDirection: 'column', alignItems: 'flex-center', justifyContent: 'flex-center', - height: claimed || success ? 'fit-content' : isSignedIn ? '8.5rem' : '12rem', + height: claimed || success ? 'fit-content' : isSignedIn ? '12rem' : '14.5rem', overflow: 'hidden', width: 'fit-content', - minWidth: '16.125rem', + minWidth: '18.5rem', gap: `${t.space.$1x5}`, padding: `${t.space.$2x5} ${t.space.$3} ${t.space.$3} ${t.space.$3}`, borderRadius: `${t.radii.$xl}`, @@ -244,9 +243,15 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { )}

{ cursor: pointer; `} > - {success ? 'Claim completed' : claimed ? 'Missing environment keys' : 'Clerk is in keyless mode'} + {success + ? 'Your app is ready' + : claimed + ? 'Missing environment keys' + : isSignedIn + ? "You've created your first user" + : 'Configure your application'}

@@ -360,7 +371,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { > {appName} {' '} - has been claimed. Configure settings from the{' '} + has been configured. You may now customize your settings in the{' '} { }, })} > - Clerk Dashboard + Clerk dashboard + .

) : claimed ? (

{ Dashboard.

) : isSignedIn ? ( -

- - You've created your first user! Link this application to your Clerk account to explore the - Dashboard. - -

+ <> +

+ Head to the dashboard to customize authentication settings, view user info, and explore more + features. +

+ + ) : ( <>

{ > Temporary API keys are enabled so you can get started immediately.

+

{ text-wrap: pretty; `} > - Claim this application to access the Clerk Dashboard where you can manage auth settings and explore - more Clerk features. + Access the dashboard to customize auth settings and explore Clerk features.

)} @@ -501,7 +544,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { } `} > - {claimed ? 'Get API keys' : 'Claim application'} + {claimed ? 'Get API keys' : 'Configure your application'} ))} From 94aace73405d510253494bc85fcafa3e2769c7e9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 6 Feb 2026 09:52:02 -0500 Subject: [PATCH 2/7] add changeset --- .changeset/puny-onions-hide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/puny-onions-hide.md diff --git a/.changeset/puny-onions-hide.md b/.changeset/puny-onions-hide.md new file mode 100644 index 00000000000..d43b5d1d811 --- /dev/null +++ b/.changeset/puny-onions-hide.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Updates keyless prompt content From 856ab817012eec4d1d8776e9a2bd9f41befd92ea Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 6 Feb 2026 15:13:55 -0500 Subject: [PATCH 3/7] update tests --- .../tests/next-quickstart-keyless.test.ts | 154 ++++++++++++++++-- .../unstable/page-objects/keylessPopover.ts | 35 +++- 2 files changed, 173 insertions(+), 16 deletions(-) diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index d143b60385e..bfd30a4713c 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -48,10 +48,22 @@ test.describe('Keyless mode @quickstart', () => { await app.teardown(); }); - test('Navigates to non-existent page (/_not-found) without a infinite redirect loop.', async ({ page, context }) => { + test.skip('Navigates to non-existent page (/_not-found) without a infinite redirect loop.', async ({ + page, + context, + }) => { + test.setTimeout(60000); // Increase timeout for this test const u = createTestUtils({ app, page, context }); await u.page.goToAppHome(); - await u.page.waitForClerkJsLoaded(); + + // Wait for Clerk.js to load - use a longer timeout for keyless mode + await page + .waitForFunction(() => window.Clerk?.loaded, { timeout: 30000 }) + .catch(() => { + // If Clerk.js doesn't load after 30s, it might be a redirect loop issue + // Continue to check for redirect loops anyway + }); + await u.po.expect.toBeSignedOut(); await u.po.keylessPopover.waitForMounted(); @@ -78,32 +90,66 @@ test.describe('Keyless mode @quickstart', () => { await u.po.keylessPopover.waitForMounted(); + // Popover now starts expanded by default + expect(await u.po.keylessPopover.isExpanded()).toBe(true); + + // Verify new content appears when expanded + const notSignedInContent = u.po.keylessPopover.getNotSignedInContent(); + await expect(notSignedInContent.temporaryKeysText).toBeVisible(); + await expect(notSignedInContent.dashboardText).toBeVisible(); + await expect(notSignedInContent.bulletList).toBeVisible(); + + // Test collapsing and expanding + await u.po.keylessPopover.toggle(); expect(await u.po.keylessPopover.isExpanded()).toBe(false); await u.po.keylessPopover.toggle(); expect(await u.po.keylessPopover.isExpanded()).toBe(true); const claim = await u.po.keylessPopover.promptsToClaim(); + // Verify the link href contains the expected claim URL pattern before clicking + const href = await claim.getAttribute('href'); + expect(href).toContain('apps/claim'); + expect(href).toContain('token='); + const [newPage] = await Promise.all([context.waitForEvent('page'), claim.click()]); await newPage.waitForLoadState(); - await newPage.waitForURL(url => { - const urlToReturnTo = `${dashboardUrl}apps/claim?token=`; + // Wait for navigation to either Clerk dashboard or Vercel SSO (which redirects to Clerk) + // The claim URL may redirect through Vercel SSO first + const urlToReturnTo = `${dashboardUrl}apps/claim?token=`; - const signUpForceRedirectUrl = url.searchParams.get('sign_up_force_redirect_url'); + await newPage.waitForURL( + url => { + // Check if we're on the Clerk dashboard claim sign-in page + if (url.hostname.includes('dashboard.clerk') && url.pathname === '/apps/claim/sign-in') { + const signUpForceRedirectUrl = url.searchParams.get('sign_up_force_redirect_url'); + const signInForceRedirectUrl = url.searchParams.get('sign_in_force_redirect_url'); - const signUpForceRedirectUrlCheck = - signUpForceRedirectUrl?.startsWith(urlToReturnTo) || - (signUpForceRedirectUrl?.startsWith(`${dashboardUrl}prepare-account`) && - signUpForceRedirectUrl?.includes(encodeURIComponent('apps/claim?token='))); + const signUpForceRedirectUrlCheck = + signUpForceRedirectUrl?.startsWith(urlToReturnTo) || + (signUpForceRedirectUrl?.startsWith(`${dashboardUrl}prepare-account`) && + signUpForceRedirectUrl?.includes(encodeURIComponent('apps/claim?token='))); - return ( - url.pathname === '/apps/claim/sign-in' && - url.searchParams.get('sign_in_force_redirect_url')?.startsWith(urlToReturnTo) && - signUpForceRedirectUrlCheck - ); - }); + return signInForceRedirectUrl?.startsWith(urlToReturnTo) && signUpForceRedirectUrlCheck; + } + + // Check if we're on Vercel SSO (which will redirect to the claim URL) + // This is acceptable as it's part of the authentication flow + if (url.hostname.includes('vercel.com') && url.pathname === '/login') { + const nextParam = url.searchParams.get('next'); + return ( + nextParam?.includes(encodeURIComponent('dashboard.clerk')) && + (nextParam?.includes(encodeURIComponent('apps/claim')) || + nextParam?.includes(encodeURIComponent('sso-api'))) + ); + } + + return false; + }, + { timeout: 30000 }, + ); }); test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({ @@ -117,6 +163,12 @@ test.describe('Keyless mode @quickstart', () => { await u.po.keylessPopover.waitForMounted(); expect(await u.po.keylessPopover.isExpanded()).toBe(true); + + // Verify claimed state content + const claimedContent = u.po.keylessPopover.getClaimedContent(); + await expect(claimedContent.title).toBeVisible(); + await expect(claimedContent.description).toBeVisible(); + await expect(u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible(); const [newPage] = await Promise.all([ @@ -130,6 +182,71 @@ test.describe('Keyless mode @quickstart', () => { }); }); + test('Signed-in user sees updated prompt content.', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + // Create and sign in a user + const fakeUser = u.services.users.createFakeUser({ + fictionalEmail: true, + withPassword: true, + }); + await u.services.users.createBapiUser(fakeUser); + + await u.page.goToAppHome(); + await u.page.waitForClerkJsLoaded(); + + // Sign in + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ + email: fakeUser.email, + password: fakeUser.password, + }); + await u.po.expect.toBeSignedIn(); + + // Navigate back to home to see the keyless prompt + await u.page.goToAppHome(); + await u.po.keylessPopover.waitForMounted(); + + // Verify prompt is expanded by default when signed in + expect(await u.po.keylessPopover.isExpanded()).toBe(true); + + // Verify signed-in content + const signedInContent = u.po.keylessPopover.getSignedInContent(); + await expect(signedInContent.title).toBeVisible(); + await expect(signedInContent.description).toBeVisible(); + await expect(signedInContent.bulletList).toBeVisible(); + + // Verify bullet items are present + await expect(signedInContent.bulletItems.first()).toBeVisible(); + + // Verify "Configure your application" button is visible + await expect(u.po.keylessPopover.promptsToClaim()).toBeVisible(); + + await fakeUser.deleteIfExists(); + }); + + test('Not signed-in user sees updated prompt content.', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToAppHome(); + await u.page.waitForClerkJsLoaded(); + await u.po.expect.toBeSignedOut(); + + await u.po.keylessPopover.waitForMounted(); + + // Popover starts expanded by default + expect(await u.po.keylessPopover.isExpanded()).toBe(true); + + // Verify not signed-in content + const notSignedInContent = u.po.keylessPopover.getNotSignedInContent(); + await expect(notSignedInContent.title).toBeVisible(); + await expect(notSignedInContent.temporaryKeysText).toBeVisible(); + await expect(notSignedInContent.bulletList).toBeVisible(); + await expect(notSignedInContent.dashboardText).toBeVisible(); + + // Verify bullet items are present + await expect(notSignedInContent.bulletItems.first()).toBeVisible(); + }); + test('Claimed application with keys inside .env, on dismiss, keyless prompt is removed.', async ({ page, context, @@ -152,6 +269,13 @@ test.describe('Keyless mode @quickstart', () => { await page.reload(); await u.po.keylessPopover.waitForMounted(); + + // Verify success state content + const successContent = u.po.keylessPopover.getSuccessContent(); + await expect(successContent.title).toBeVisible(); + await expect(successContent.configuredText).toBeVisible(); + await expect(successContent.dashboardLink).toBeVisible(); + await u.po.keylessPopover.promptToDismiss().click(); await u.po.keylessPopover.waitForUnmounted(); diff --git a/packages/testing/src/playwright/unstable/page-objects/keylessPopover.ts b/packages/testing/src/playwright/unstable/page-objects/keylessPopover.ts index 69b5bbd4728..e699f0b8699 100644 --- a/packages/testing/src/playwright/unstable/page-objects/keylessPopover.ts +++ b/packages/testing/src/playwright/unstable/page-objects/keylessPopover.ts @@ -4,6 +4,7 @@ export const createKeylessPopoverPageObject = (testArgs: { page: EnhancedPage }) const { page } = testArgs; // TODO: Is this the ID we really want ? const elementId = '#--clerk-keyless-prompt-button'; + const contentId = '#--clerk-keyless-prompt-content'; const self = { waitForMounted: () => page.waitForSelector(elementId, { state: 'attached' }), waitForUnmounted: () => page.waitForSelector(elementId, { state: 'detached' }), @@ -15,7 +16,7 @@ export const createKeylessPopoverPageObject = (testArgs: { page: EnhancedPage }) toggle: () => page.locator(elementId).click(), promptsToClaim: () => { - return page.getByRole('link', { name: /^claim application$/i }); + return page.getByRole('link', { name: /^configure your application$/i }); }, promptToUseClaimedKeys: () => { return page.getByRole('link', { name: /^get api keys$/i }); @@ -23,6 +24,38 @@ export const createKeylessPopoverPageObject = (testArgs: { page: EnhancedPage }) promptToDismiss: () => { return page.getByRole('button', { name: /^dismiss$/i }); }, + + // Helper methods to check content text for different states + getSignedInContent: () => { + return { + title: page.getByText("You've created your first user", { exact: false }), + description: page.getByText(/Head to the dashboard to customize authentication settings/i), + bulletList: page.locator(contentId).locator('ul'), + bulletItems: page.locator(contentId).getByText(/Add SSO connections|Set up B2B authentication|Enable MFA/i), + }; + }, + getNotSignedInContent: () => { + return { + title: page.locator(elementId).getByText('Configure your application', { exact: false }), + temporaryKeysText: page.getByText(/Temporary API keys are enabled/i), + bulletList: page.locator(contentId).locator('ul'), + bulletItems: page.locator(contentId).getByText(/Add SSO connections|Set up B2B authentication|Enable MFA/i), + dashboardText: page.getByText(/Access the dashboard to customize auth settings/i), + }; + }, + getSuccessContent: () => { + return { + title: page.getByText('Your app is ready', { exact: false }), + configuredText: page.getByText(/has been configured/i), + dashboardLink: page.getByRole('link', { name: /Clerk dashboard/i }), + }; + }, + getClaimedContent: () => { + return { + title: page.getByText('Missing environment keys', { exact: false }), + description: page.getByText(/You claimed this application but haven't set keys/i), + }; + }, }; return self; }; From 77afb233739e5729f5e9bfcac55f20264f932ba5 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 6 Feb 2026 15:19:37 -0500 Subject: [PATCH 4/7] Update next-quickstart-keyless.test.ts --- .../tests/next-quickstart-keyless.test.ts | 71 +++++-------------- 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index bfd30a4713c..77b984e5e24 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -48,22 +48,10 @@ test.describe('Keyless mode @quickstart', () => { await app.teardown(); }); - test.skip('Navigates to non-existent page (/_not-found) without a infinite redirect loop.', async ({ - page, - context, - }) => { - test.setTimeout(60000); // Increase timeout for this test + test('Navigates to non-existent page (/_not-found) without a infinite redirect loop.', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.page.goToAppHome(); - - // Wait for Clerk.js to load - use a longer timeout for keyless mode - await page - .waitForFunction(() => window.Clerk?.loaded, { timeout: 30000 }) - .catch(() => { - // If Clerk.js doesn't load after 30s, it might be a redirect loop issue - // Continue to check for redirect loops anyway - }); - + await u.page.waitForClerkJsLoaded(); await u.po.expect.toBeSignedOut(); await u.po.keylessPopover.waitForMounted(); @@ -107,49 +95,26 @@ test.describe('Keyless mode @quickstart', () => { const claim = await u.po.keylessPopover.promptsToClaim(); - // Verify the link href contains the expected claim URL pattern before clicking - const href = await claim.getAttribute('href'); - expect(href).toContain('apps/claim'); - expect(href).toContain('token='); - const [newPage] = await Promise.all([context.waitForEvent('page'), claim.click()]); await newPage.waitForLoadState(); - // Wait for navigation to either Clerk dashboard or Vercel SSO (which redirects to Clerk) - // The claim URL may redirect through Vercel SSO first - const urlToReturnTo = `${dashboardUrl}apps/claim?token=`; - - await newPage.waitForURL( - url => { - // Check if we're on the Clerk dashboard claim sign-in page - if (url.hostname.includes('dashboard.clerk') && url.pathname === '/apps/claim/sign-in') { - const signUpForceRedirectUrl = url.searchParams.get('sign_up_force_redirect_url'); - const signInForceRedirectUrl = url.searchParams.get('sign_in_force_redirect_url'); - - const signUpForceRedirectUrlCheck = - signUpForceRedirectUrl?.startsWith(urlToReturnTo) || - (signUpForceRedirectUrl?.startsWith(`${dashboardUrl}prepare-account`) && - signUpForceRedirectUrl?.includes(encodeURIComponent('apps/claim?token='))); - - return signInForceRedirectUrl?.startsWith(urlToReturnTo) && signUpForceRedirectUrlCheck; - } - - // Check if we're on Vercel SSO (which will redirect to the claim URL) - // This is acceptable as it's part of the authentication flow - if (url.hostname.includes('vercel.com') && url.pathname === '/login') { - const nextParam = url.searchParams.get('next'); - return ( - nextParam?.includes(encodeURIComponent('dashboard.clerk')) && - (nextParam?.includes(encodeURIComponent('apps/claim')) || - nextParam?.includes(encodeURIComponent('sso-api'))) - ); - } - - return false; - }, - { timeout: 30000 }, - ); + await newPage.waitForURL(url => { + const urlToReturnTo = `${dashboardUrl}apps/claim?token=`; + + const signUpForceRedirectUrl = url.searchParams.get('sign_up_force_redirect_url'); + + const signUpForceRedirectUrlCheck = + signUpForceRedirectUrl?.startsWith(urlToReturnTo) || + (signUpForceRedirectUrl?.startsWith(`${dashboardUrl}prepare-account`) && + signUpForceRedirectUrl?.includes(encodeURIComponent('apps/claim?token='))); + + return ( + url.pathname === '/apps/claim/sign-in' && + url.searchParams.get('sign_in_force_redirect_url')?.startsWith(urlToReturnTo) && + signUpForceRedirectUrlCheck + ); + }); }); test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({ From cf4e0ca75098d052a1fe486f48f10009e49f8930 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 6 Feb 2026 15:30:14 -0500 Subject: [PATCH 5/7] skip --- integration/tests/next-quickstart-keyless.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index 77b984e5e24..f3d35de2793 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -147,7 +147,10 @@ test.describe('Keyless mode @quickstart', () => { }); }); - test('Signed-in user sees updated prompt content.', async ({ page, context }) => { + // Skipped: This test requires creating a user via backend API, which needs CLERK_SECRET_KEY. + // Keyless mode is designed to work without keys, so we skip this test for now. + // TODO: Revisit when we have a way to test signed-in states in keyless mode without backend API access. + test.skip('Signed-in user sees updated prompt content.', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); // Create and sign in a user From 706c6a97b0667a17446ff4068afc465efea59bb1 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 6 Feb 2026 15:44:44 -0500 Subject: [PATCH 6/7] Update next-quickstart-keyless.test.ts --- integration/tests/next-quickstart-keyless.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index f3d35de2793..a3c1f395924 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -242,7 +242,6 @@ test.describe('Keyless mode @quickstart', () => { const successContent = u.po.keylessPopover.getSuccessContent(); await expect(successContent.title).toBeVisible(); await expect(successContent.configuredText).toBeVisible(); - await expect(successContent.dashboardLink).toBeVisible(); await u.po.keylessPopover.promptToDismiss().click(); From 84dfd10a8d133be545ce735ef905380b6b29ea69 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 6 Feb 2026 16:52:00 -0500 Subject: [PATCH 7/7] Update index.tsx --- .../devPrompts/KeylessPrompt/index.tsx | 254 ++++++++++-------- 1 file changed, 147 insertions(+), 107 deletions(-) diff --git a/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx b/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx index dc73ac95c5a..258a20309be 100644 --- a/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx +++ b/packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx @@ -24,13 +24,114 @@ type KeylessPromptProps = { onDismiss: (() => Promise) | undefined | null; }; +export type KeylessPromptState = 'default' | 'signedIn' | 'claimed' | 'success'; + +export interface SizingConfig { + collapsed: { + height: string; + minWidth: string; + }; + expanded: { + height: string; + minWidth: string; + }; +} + +const COLLAPSED_PADDING_LEFT = '0.75rem'; +const EXPANDED_PADDING = '0.625rem 0.75rem 0.75rem 0.75rem'; + +const contentTextStyles = css` + ${basePromptElementStyles}; + color: #b4b4b4; + font-size: 0.8125rem; + font-weight: 400; + line-height: 1rem; +`; + +const bulletListStyles = css` + ${basePromptElementStyles}; + color: #b4b4b4; + font-size: 0.8125rem; + font-weight: 400; + line-height: 1rem; + margin: 0; + padding-left: 1.25rem; + list-style: disc; +`; + +export const STATE_SIZING_CONFIG: Record = { + default: { + collapsed: { + height: '2.5rem', + minWidth: '15.5rem', + }, + expanded: { + height: '14.5rem', + minWidth: '16.5rem', + }, + }, + signedIn: { + collapsed: { + height: '2.5rem', + minWidth: '15.75rem', + }, + expanded: { + height: '12rem', + minWidth: '17rem', + }, + }, + claimed: { + collapsed: { + height: '2.5rem', + minWidth: '15.5rem', + }, + expanded: { + height: 'fit-content', + minWidth: '16.5rem', + }, + }, + success: { + collapsed: { + height: '2.5rem', + minWidth: '15.5rem', + }, + expanded: { + height: 'fit-content', + minWidth: '16.5rem', + }, + }, +}; + const buttonIdentifierPrefix = `--clerk-keyless-prompt`; const buttonIdentifier = `${buttonIdentifierPrefix}-button`; const contentIdentifier = `${buttonIdentifierPrefix}-content`; -/** - * If we cannot reconstruct the url properly, then simply fallback to Clerk Dashboard - */ +function getButtonLabel(success: boolean, claimed: boolean, isSignedIn: boolean): string { + if (success) { + return 'Your app is ready'; + } + if (claimed) { + return 'Missing environment keys'; + } + if (isSignedIn) { + return "You've created your first user"; + } + return 'Configure your application'; +} + +function determineState(claimed: boolean, success: boolean, isSignedIn: boolean): KeylessPromptState { + if (success) { + return 'success'; + } + if (claimed) { + return 'claimed'; + } + if (isSignedIn) { + return 'signedIn'; + } + return 'default'; +} + function withLastActiveFallback(cb: () => string): string { try { return cb(); @@ -39,42 +140,48 @@ function withLastActiveFallback(cb: () => string): string { } } -const KeylessPromptInternal = (_props: KeylessPromptProps) => { +const KeylessPromptInternal = (props: KeylessPromptProps) => { const { isSignedIn } = useUser(); const [isExpanded, setIsExpanded] = useState(true); - useEffect(() => { - if (isSignedIn) { - setIsExpanded(true); - } - }, [isSignedIn]); - const environment = useRevalidateEnvironment(); const claimed = Boolean(environment.authConfig.claimedAt); - const success = typeof _props.onDismiss === 'function' && claimed; + const success = typeof props.onDismiss === 'function' && claimed; const appName = environment.displayConfig.applicationName; + const isSignedInBoolean = Boolean(isSignedIn); + + const state = useMemo( + () => determineState(claimed, success, isSignedInBoolean), + [claimed, success, isSignedInBoolean], + ); + const sizingConfig = useMemo(() => STATE_SIZING_CONFIG[state], [state]); + + useEffect(() => { + if (isSignedInBoolean) { + setIsExpanded(true); + } + }, [isSignedInBoolean]); const isForcedExpanded = claimed || success || isExpanded; + const claimUrlToDashboard = useMemo(() => { if (claimed) { - return _props.copyKeysUrl; + return props.copyKeysUrl; } - - const url = new URL(_props.claimUrl); - // Clerk Dashboard accepts a `return_url` query param when visiting `/apps/claim`. + const url = new URL(props.claimUrl); url.searchParams.append('return_url', window.location.href); return url.href; - }, [claimed, _props.copyKeysUrl, _props.claimUrl]); + }, [claimed, props.copyKeysUrl, props.claimUrl]); const instanceUrlToDashboard = useMemo(() => { return withLastActiveFallback(() => { - const redirectUrlParts = handleDashboardUrlParsing(_props.copyKeysUrl); + const redirectUrlParts = handleDashboardUrlParsing(props.copyKeysUrl); const url = new URL( `${redirectUrlParts.baseDomain}/apps/${redirectUrlParts.appId}/instances/${redirectUrlParts.instanceId}/user-authentication/email-phone-username`, ); return url.href; }); - }, [_props.copyKeysUrl]); + }, [props.copyKeysUrl]); const mainCTAStyles = css` ${basePromptElementStyles}; @@ -88,7 +195,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.12px; - color: ${claimed ? 'white' : success ? 'white' : '#fde047'}; + color: ${success || claimed ? 'white' : '#fde047'}; text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.32); white-space: nowrap; user-select: none; @@ -110,9 +217,9 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { position: 'fixed', bottom: '1.25rem', right: '1.25rem', - height: `${t.sizes.$10}`, - minWidth: '15.5rem', - paddingLeft: `${t.space.$3}`, + height: sizingConfig.collapsed.height, + minWidth: sizingConfig.collapsed.minWidth, + paddingLeft: COLLAPSED_PADDING_LEFT, borderRadius: '1.25rem', transition: 'all 195ms cubic-bezier(0.2, 0.61, 0.1, 1)', @@ -124,12 +231,13 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { flexDirection: 'column', alignItems: 'flex-center', justifyContent: 'flex-center', - height: claimed || success ? 'fit-content' : isSignedIn ? '12rem' : '14.5rem', + height: sizingConfig.expanded.height, overflow: 'hidden', width: 'fit-content', - minWidth: '18.5rem', + minWidth: sizingConfig.expanded.minWidth, + paddingLeft: undefined, + padding: EXPANDED_PADDING, gap: `${t.space.$1x5}`, - padding: `${t.space.$2x5} ${t.space.$3} ${t.space.$3} ${t.space.$3}`, borderRadius: `${t.radii.$xl}`, transition: 'all 230ms cubic-bezier(0.28, 1, 0.32, 1)', }, @@ -244,15 +352,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => {

{ cursor: pointer; `} > - {success - ? 'Your app is ready' - : claimed - ? 'Missing environment keys' - : isSignedIn - ? "You've created your first user" - : 'Configure your application'} + {getButtonLabel(success, claimed, isSignedInBoolean)}

@@ -330,7 +424,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { flex-direction: column; gap: 0.5rem; color: #b4b4b4; - max-width: 14.625rem; + max-width: 15rem; animation: ${isForcedExpanded && 'show-description 500ms ease-in forwards'}; @keyframes show-description { 0%, @@ -345,15 +439,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { `} > {success ? ( -

+

Your application{' '} { font-size: 0.8125rem; font-weight: 500; color: #d5d5d5; + line-height: inherit; `} > {appName} @@ -390,44 +477,17 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { .

) : claimed ? ( -

+

You claimed this application but haven't set keys in your environment. Get them from the Clerk Dashboard.

- ) : isSignedIn ? ( + ) : isSignedInBoolean ? ( <> -

+

Head to the dashboard to customize authentication settings, view user info, and explore more features.

-
    +
    • Add SSO connections (eg. GitHub)
    • Set up B2B authentication
    • Enable MFA
    • @@ -437,39 +497,20 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { <>

      Temporary API keys are enabled so you can get started immediately.

      -
        +
        • Add SSO connections (eg. GitHub)
        • Set up B2B authentication
        • Enable MFA

        @@ -485,7 +526,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => {