diff --git a/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx b/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx index 9844b8ef814..c13ffd7b427 100644 --- a/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx +++ b/packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx @@ -2,7 +2,7 @@ import { useUser } from '@clerk/shared/react'; // eslint-disable-next-line no-restricted-imports import { css } from '@emotion/react'; import type { PropsWithChildren } from 'react'; -import { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { Flex, Link } from '../../../customizables'; @@ -28,6 +28,12 @@ const buttonIdentifierPrefix = `--clerk-keyless-prompt`; const buttonIdentifier = `${buttonIdentifierPrefix}-button`; const contentIdentifier = `${buttonIdentifierPrefix}-content`; +// Animation timing constants +const ANIMATION_DURATION = '200ms'; +const CONTENT_FADE_DURATION = '200ms'; +const CONTENT_FADE_DELAY = '40ms'; +const EASING_CURVE = 'cubic-bezier(0.2, 0, 0, 1)'; + /** * If we cannot reconstruct the url properly, then simply fallback to Clerk Dashboard */ @@ -39,9 +45,10 @@ function withLastActiveFallback(cb: () => string): string { } } -const KeylessPromptInternal = (_props: KeylessPromptProps) => { +const _KeylessPromptInternal = (_props: KeylessPromptProps) => { const { isSignedIn } = useUser(); const [isExpanded, setIsExpanded] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); useEffect(() => { if (isSignedIn) { @@ -49,12 +56,22 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { } }, [isSignedIn]); + // Track animation state to prevent interactions during transition + useEffect(() => { + if (!isAnimating) { + return; + } + const timer = setTimeout(() => setIsAnimating(false), 200); + return () => clearTimeout(timer); + }, [isAnimating]); + const environment = useRevalidateEnvironment(); const claimed = Boolean(environment.authConfig.claimedAt); const success = typeof _props.onDismiss === 'function' && claimed; const appName = environment.displayConfig.applicationName; const isForcedExpanded = claimed || success || isExpanded; + const claimUrlToDashboard = useMemo(() => { if (claimed) { return _props.copyKeysUrl; @@ -76,13 +93,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { }); }, [_props.copyKeysUrl]); - const getKeysUrlFromLastActive = useMemo(() => { - return withLastActiveFallback(() => { - const redirectUrlParts = handleDashboardUrlParsing(_props.copyKeysUrl); - const url = new URL(`${redirectUrlParts.baseDomain}/last-active?path=api-keys`); - return url.href; - }); - }, [_props.copyKeysUrl]); + const ctaButtonColor = claimed || success ? 'white' : '#fde047'; const mainCTAStyles = css` ${basePromptElementStyles}; @@ -97,7 +108,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { font-size: 0.75rem; font-weight: 500; letter-spacing: 0.12px; - color: ${claimed ? 'white' : success ? 'white' : '#fde047'}; + color: ${ctaButtonColor}; text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.32); white-space: nowrap; user-select: none; @@ -111,347 +122,364 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => { 0px 0px 4px 0px rgba(243, 107, 22, 0) inset; `; + const ctaButtonHoverStyles = claimed + ? 'background: #4B4B4B; transition: all 120ms ease-in-out;' + : `box-shadow: + 0px 0px 6px 0px rgba(253, 224, 71, 0.24) inset, + 0px 0px 0px 1px rgba(255, 255, 255, 0.04) inset, + 0px 1px 0px 0px rgba(255, 255, 255, 0.04) inset, + 0px 0px 0px 1px rgba(0, 0, 0, 0.12), + 0px 1.5px 2px 0px rgba(0, 0, 0, 0.48);`; + + function renderStatusIcon() { + if (success) { + return ( + + ); + } + + if (claimed) { + return ( + + + + ); + } + + return ( +
+ + + + + + + +
+ ); + } + + function getStatusText() { + if (success) { + return 'Claim completed'; + } + if (claimed) { + return 'Missing environment keys'; + } + return 'Clerk is in keyless mode'; + } + + const contentParagraphStyles = css` + ${basePromptElementStyles}; + color: #b4b4b4; + font-size: 0.8125rem; + font-weight: 400; + line-height: 1rem; + `; + + const titleTextStyles = css` + ${basePromptElementStyles}; + color: #d9d9d9; + font-size: 0.875rem; + font-weight: 500; + white-space: nowrap; + cursor: pointer; + `; + return ( ({ + interpolateSize: 'allow-keywords', position: 'fixed', bottom: '1.25rem', right: '1.25rem', - height: `${t.sizes.$10}`, - minWidth: '13.4rem', - paddingLeft: `${t.space.$3}`, + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'flex-start', + overflow: 'clip', + width: '13.5rem', + height: '2.375rem', borderRadius: '1.25rem', - transition: 'all 195ms cubic-bezier(0.2, 0.61, 0.1, 1)', + transition: `width ${ANIMATION_DURATION} ${EASING_CURVE}, + height ${ANIMATION_DURATION} ${EASING_CURVE}, + padding ${ANIMATION_DURATION} ${EASING_CURVE}, + border-radius ${ANIMATION_DURATION} ${EASING_CURVE}, + background ${ANIMATION_DURATION} ${EASING_CURVE}`, '&[data-expanded="false"]:hover': { background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.20) 0%, rgba(255, 255, 255, 0) 100%), #1f1f1f', }, '&[data-expanded="true"]': { - flexDirection: 'column', - alignItems: 'flex-center', - justifyContent: 'flex-center', - height: claimed || success ? 'fit-content' : isSignedIn ? '8.5rem' : '12rem', - overflow: 'hidden', - width: 'fit-content', - minWidth: '16.125rem', - gap: `${t.space.$1x5}`, - padding: `${t.space.$2x5} ${t.space.$3} ${t.space.$3} ${t.space.$3}`, + width: '16.125rem', + height: 'fit-content', borderRadius: `${t.radii.$xl}`, - transition: 'all 230ms cubic-bezier(0.28, 1, 0.32, 1)', }, })} > - - ({ - flexDirection: 'column', - gap: t.space.$3, - })} - > -