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 diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index d143b60385e..a3c1f395924 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -78,6 +78,17 @@ 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); @@ -117,6 +128,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 +147,74 @@ test.describe('Keyless mode @quickstart', () => { }); }); + // 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 + 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 +237,12 @@ 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 u.po.keylessPopover.promptToDismiss().click(); await u.po.keylessPopover.waitForUnmounted(); 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..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(false); - - useEffect(() => { - if (isSignedIn) { - setIsExpanded(true); - } - }, [isSignedIn]); + const [isExpanded, setIsExpanded] = useState(true); 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}; @@ -83,13 +190,12 @@ 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; 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; @@ -111,9 +217,9 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { position: 'fixed', bottom: '1.25rem', right: '1.25rem', - height: `${t.sizes.$10}`, - minWidth: '13.4rem', - 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)', @@ -125,12 +231,13 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { flexDirection: 'column', alignItems: 'flex-center', justifyContent: 'flex-center', - height: claimed || success ? 'fit-content' : isSignedIn ? '8.5rem' : '12rem', + height: sizingConfig.expanded.height, overflow: 'hidden', width: 'fit-content', - minWidth: '16.125rem', + 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,10 +351,8 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { )}

{ cursor: pointer; `} > - {success ? 'Claim completed' : claimed ? 'Missing environment keys' : 'Clerk is in keyless mode'} + {getButtonLabel(success, claimed, isSignedInBoolean)}

@@ -319,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%, @@ -334,15 +439,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { `} > {success ? ( -

+

Your application{' '} { font-size: 0.8125rem; font-weight: 500; color: #d5d5d5; + line-height: inherit; `} > {appName} {' '} - has been claimed. Configure settings from the{' '} + has been configured. You may now customize your settings in the{' '} { }, })} > - Clerk Dashboard + Clerk dashboard + .

) : claimed ? ( -

+

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

- ) : isSignedIn ? ( -

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

+ ) : isSignedInBoolean ? ( + <> +

+ 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.

+

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

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