From 4a5b21a204774ca1a06cf3dd35e3284271231c1a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 11:36:02 -0500 Subject: [PATCH 01/16] feat(ui): Extract `` component --- packages/ui/src/common/ProviderIcon.tsx | 85 +++++++++++++++++++ packages/ui/src/common/index.ts | 1 + packages/ui/src/common/providerIconUtils.ts | 11 +++ .../UserProfile/ConnectedAccountsMenu.tsx | 22 ++--- .../UserProfile/ConnectedAccountsSection.tsx | 30 ++++--- .../UserProfile/EnterpriseAccountsSection.tsx | 29 ++----- .../src/components/UserProfile/Web3Form.tsx | 15 ++-- .../components/UserProfile/Web3Section.tsx | 10 ++- packages/ui/src/elements/SocialButtons.tsx | 47 ++-------- .../src/elements/Web3SolanaWalletButtons.tsx | 38 +++++---- 10 files changed, 174 insertions(+), 114 deletions(-) create mode 100644 packages/ui/src/common/ProviderIcon.tsx create mode 100644 packages/ui/src/common/providerIconUtils.ts diff --git a/packages/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx new file mode 100644 index 00000000000..71b86ebc977 --- /dev/null +++ b/packages/ui/src/common/ProviderIcon.tsx @@ -0,0 +1,85 @@ +import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/shared/types'; +import React from 'react'; + +import { descriptors, Span } from '../customizables'; +import type { PropsOfComponent } from '../styledSystem'; +import { ProviderInitialIcon } from './ProviderInitialIcon'; +import { supportsMaskImage } from './providerIconUtils'; + +export type ProviderIconProps = Omit< + PropsOfComponent, + 'elementDescriptor' | 'elementId' | 'aria-label' +> & { + id: OAuthProvider | Web3Provider | PhoneCodeChannel; + iconUrl?: string | null; + name: string; + size?: string; + isLoading?: boolean; + isDisabled?: boolean; + alt?: string; + elementDescriptor?: any; + elementId?: any; +}; + +export const ProviderIcon = (props: ProviderIconProps) => { + const { + id, + iconUrl, + name, + size = '$4', + isLoading, + isDisabled, + alt, + elementDescriptor = descriptors.providerIcon, + elementId, + sx, + ...rest + } = props; + + // If no iconUrl or empty string, fallback to ProviderInitialIcon + if (!iconUrl || iconUrl.trim() === '') { + return ( + + ); + } + + // Use Span with maskImage or backgroundImage based on provider support + return ( + ({ + display: 'inline-block', + width: theme.sizes[size as keyof typeof theme.sizes] || size, + height: theme.sizes[size as keyof typeof theme.sizes] || size, + maxWidth: '100%', + opacity: isLoading || isDisabled ? 0.5 : 1, + ...(supportsMaskImage(id) + ? { + '--cl-icon-fill': theme.colors.$colorForeground, + backgroundColor: 'var(--cl-icon-fill)', + maskImage: `url(${iconUrl})`, + maskSize: 'cover', + maskPosition: 'center', + maskRepeat: 'no-repeat', + } + : { + backgroundImage: `url(${iconUrl})`, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + }), + ...(typeof sx === 'function' ? sx(theme) : sx), + })} + {...rest} + /> + ); +}; diff --git a/packages/ui/src/common/index.ts b/packages/ui/src/common/index.ts index e1a892faa43..f859201d4b6 100644 --- a/packages/ui/src/common/index.ts +++ b/packages/ui/src/common/index.ts @@ -8,6 +8,7 @@ export * from './InfiniteListSpinner'; export * from './NotificationCountBadge'; export * from './PrintableComponent'; export * from './ProviderInitialIcon'; +export * from './ProviderIcon'; export * from './QRCode'; export * from './redirects'; export * from './RemoveResourceForm'; diff --git a/packages/ui/src/common/providerIconUtils.ts b/packages/ui/src/common/providerIconUtils.ts new file mode 100644 index 00000000000..99a75a6a656 --- /dev/null +++ b/packages/ui/src/common/providerIconUtils.ts @@ -0,0 +1,11 @@ +import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/shared/types'; + +export const SUPPORTS_MASK_IMAGE = ['apple', 'github', 'okx_wallet', 'vercel'] as const; + +type SupportsMaskImageProvider = (typeof SUPPORTS_MASK_IMAGE)[number]; + +export const supportsMaskImage = ( + id: OAuthProvider | Web3Provider | PhoneCodeChannel, +): id is SupportsMaskImageProvider => { + return (SUPPORTS_MASK_IMAGE as readonly string[]).includes(id); +}; diff --git a/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx b/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx index c821739d1ef..fdc467e22a0 100644 --- a/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx +++ b/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx @@ -7,9 +7,9 @@ import { ProfileSection } from '@/ui/elements/Section'; import { handleError } from '@/ui/utils/errorHandler'; import { sleep } from '@/ui/utils/sleep'; -import { ProviderInitialIcon } from '../../common'; +import { ProviderIcon } from '../../common'; import { useUserProfileContext } from '../../contexts'; -import { descriptors, Image, localizationKeys } from '../../customizables'; +import { descriptors, localizationKeys } from '../../customizables'; import { useEnabledThirdPartyProviders } from '../../hooks'; import { useRouter } from '../../router'; @@ -36,22 +36,16 @@ const ConnectMenuButton = (props: { strategy: OAuthStrategy; onClick?: () => voi }); }); - const imageOrInitial = strategyToDisplayData[strategy].iconUrl ? ( - {`Connect ({ width: theme.sizes.$4 })} - /> - ) : ( - ); diff --git a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx index eaa0e746ba4..79447792813 100644 --- a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx +++ b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx @@ -9,9 +9,9 @@ import { ProfileSection } from '@/ui/elements/Section'; import { ThreeDotsMenu } from '@/ui/elements/ThreeDotsMenu'; import { handleError } from '@/ui/utils/errorHandler'; -import { ProviderInitialIcon } from '../../common'; +import { ProviderIcon } from '../../common'; import { useUserProfileContext } from '../../contexts'; -import { Box, Button, descriptors, Flex, Image, localizationKeys, Text } from '../../customizables'; +import { Box, Button, descriptors, Flex, localizationKeys, Text } from '../../customizables'; import { Action } from '../../elements/Action'; import { useActionContext } from '../../elements/Action/ActionRoot'; import { useEnabledThirdPartyProviders } from '../../hooks'; @@ -153,21 +153,23 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => } }; - const ImageOrInitial = () => - providerToDisplayData[account.provider].iconUrl ? ( - {providerToDisplayData[account.provider].name} ({ width: theme.sizes.$4, flexShrink: 0 })} - /> - ) : ( - { + const providerData = providerToDisplayData[account.provider]; + if (!providerData) { + return null; + } + return ( + ({ flexShrink: 0 })} /> ); + }; return ( diff --git a/packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx b/packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx index b50311e9875..49654e7c6f9 100644 --- a/packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx +++ b/packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx @@ -3,8 +3,8 @@ import type { EnterpriseAccountResource, OAuthProvider } from '@clerk/shared/typ import { ProfileSection } from '@/ui/elements/Section'; -import { ProviderInitialIcon } from '../../common'; -import { Badge, Box, descriptors, Flex, Image, localizationKeys, Text } from '../../customizables'; +import { ProviderIcon } from '../../common'; +import { Badge, Box, descriptors, Flex, localizationKeys, Text } from '../../customizables'; export const EnterpriseAccountsSection = () => { const { user } = useUser(); @@ -86,25 +86,14 @@ const EnterpriseAccountProviderIcon = ({ account }: { account: EnterpriseAccount const providerWithoutPrefix = provider.replace(/(oauth_|saml_)/, '').trim() as OAuthProvider; const connectionName = enterpriseConnection?.name ?? providerWithoutPrefix; - const commonImageProps = { - elementDescriptor: [descriptors.providerIcon], - alt: connectionName, - sx: (theme: any) => ({ width: theme.sizes.$4 }), - elementId: descriptors.enterpriseButtonsProviderIcon.setId(account.provider), - }; - - return enterpriseConnection?.logoPublicUrl ? ( - - ) : ( - ); }; diff --git a/packages/ui/src/components/UserProfile/Web3Form.tsx b/packages/ui/src/components/UserProfile/Web3Form.tsx index a2765b22f44..b5cf65df9b8 100644 --- a/packages/ui/src/components/UserProfile/Web3Form.tsx +++ b/packages/ui/src/components/UserProfile/Web3Form.tsx @@ -3,8 +3,9 @@ import { useReverification, useUser } from '@clerk/shared/react'; import type { Web3Provider, Web3Strategy } from '@clerk/shared/types'; import { useModuleManager } from '@/contexts'; -import { descriptors, Image, localizationKeys } from '@/customizables'; +import { descriptors, localizationKeys } from '@/customizables'; import { useEnabledThirdPartyProviders } from '@/hooks'; +import { ProviderIcon } from '@/ui/common'; import { Web3SelectSolanaWalletScreen } from '@/ui/components/UserProfile/Web3SelectSolanaWalletScreen'; import { Action } from '@/ui/elements/Action'; import { useActionContext } from '@/ui/elements/Action/ActionRoot'; @@ -91,14 +92,16 @@ export const AddWeb3WalletActionMenu = () => { gap: t.space.$2, })} leftIcon={ - {`Connect ({ width: theme.sizes.$5 })} + size='$5' + elementDescriptor={descriptors.providerIcon} + elementId={descriptors.providerIcon.setId(strategyToDisplayData[strategy].id)} /> } /> diff --git a/packages/ui/src/components/UserProfile/Web3Section.tsx b/packages/ui/src/components/UserProfile/Web3Section.tsx index 525246c6bd0..9dd7938f939 100644 --- a/packages/ui/src/components/UserProfile/Web3Section.tsx +++ b/packages/ui/src/components/UserProfile/Web3Section.tsx @@ -7,7 +7,8 @@ import { ProfileSection } from '@/ui/elements/Section'; import { ThreeDotsMenu } from '@/ui/elements/ThreeDotsMenu'; import { handleError } from '@/ui/utils/errorHandler'; -import { Badge, Box, Flex, Image, localizationKeys, Text } from '../../customizables'; +import { ProviderIcon } from '../../common'; +import { Badge, Box, Flex, localizationKeys, Text } from '../../customizables'; import { Action } from '../../elements/Action'; import { useActionContext } from '../../elements/Action/ActionRoot'; import { useEnabledThirdPartyProviders } from '../../hooks'; @@ -72,10 +73,11 @@ export const Web3Section = withCardStateProvider( > ({ alignItems: 'center', gap: t.space.$2, width: '100%' })}> {strategyToDisplayData[strategy].iconUrl && ( - {strategyToDisplayData[strategy].name} ({ width: theme.sizes.$4 })} /> )} diff --git a/packages/ui/src/elements/SocialButtons.tsx b/packages/ui/src/elements/SocialButtons.tsx index a9578e4b6b9..8c9d5b44635 100644 --- a/packages/ui/src/elements/SocialButtons.tsx +++ b/packages/ui/src/elements/SocialButtons.tsx @@ -4,7 +4,7 @@ import type { OAuthProvider, OAuthStrategy, PhoneCodeChannel, Web3Provider, Web3 import type { Ref } from 'react'; import React, { forwardRef, isValidElement } from 'react'; -import { ProviderInitialIcon } from '../common'; +import { ProviderIcon } from '../common'; import type { LocalizationKey } from '../customizables'; import { Button, @@ -14,7 +14,6 @@ import { Icon, localizationKeys, SimpleButton, - Span, Spinner, Text, useAppearance, @@ -30,13 +29,6 @@ import { distributeStrategiesIntoRows } from './utils'; const SOCIAL_BUTTON_BLOCK_THRESHOLD = 2; const SOCIAL_BUTTON_PRE_TEXT_THRESHOLD = 1; const MAX_STRATEGIES_PER_ROW = 5; -const SUPPORTS_MASK_IMAGE = ['apple', 'github', 'okx_wallet', 'vercel'] as const; - -type SupportsMaskImageProvider = (typeof SUPPORTS_MASK_IMAGE)[number]; - -const supportsMaskImage = (id: OAuthProvider | Web3Provider | PhoneCodeChannel): id is SupportsMaskImageProvider => { - return (SUPPORTS_MASK_IMAGE as readonly string[]).includes(id); -}; export type SocialButtonsProps = React.PropsWithChildren<{ enableOAuthProviders: boolean; @@ -195,39 +187,16 @@ export const SocialButtons = React.memo((props: SocialButtonsRootProps) => { provider: strategyToDisplayData[strategy].name, }); - const imageOrInitial = strategyToDisplayData[strategy].iconUrl ? ( - ({ - display: 'inline-block', - width: theme.sizes.$4, - height: theme.sizes.$4, - maxWidth: '100%', - ...(supportsMaskImage(strategyToDisplayData[strategy].id) - ? { - '--cl-icon-fill': theme.colors.$colorForeground, - backgroundColor: 'var(--cl-icon-fill)', - maskImage: `url(${strategyToDisplayData[strategy].iconUrl})`, - maskSize: 'cover', - maskPosition: 'center', - maskRepeat: 'no-repeat', - } - : { - backgroundImage: `url(${strategyToDisplayData[strategy].iconUrl})`, - backgroundSize: 'cover', - backgroundPosition: 'center', - backgroundRepeat: 'no-repeat', - }), - })} - /> - ) : ( - ); diff --git a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx index cfa88a47b0b..9ab2d5d0834 100644 --- a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx +++ b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx @@ -4,6 +4,7 @@ import { MAINNET_ENDPOINT } from '@solana/wallet-standard'; import type { Ref } from 'react'; import React, { forwardRef, isValidElement, useMemo } from 'react'; +import { ProviderIcon } from '@/ui/common/ProviderIcon'; import { WalletInitialIcon } from '@/ui/common/WalletInitialIcon'; import { Button, @@ -11,7 +12,6 @@ import { Flex, Grid, Icon, - Image, localizationKeys, SimpleButton, Spinner, @@ -130,22 +130,26 @@ const Web3SolanaWalletButtonsInner = ({ web3AuthCallback }: Web3WalletButtonsPro ? localizationKeys('web3SolanaWalletButtons.continue', { walletName: w.name }) : w.name; - const imageOrInitial = w.icon ? ( - {t(localizationKeys('web3SolanaWalletButtons.connect', ({ width: theme.sizes.$4, height: 'auto', maxWidth: '100%' })} - /> - ) : ( - - ); + const imageOrInitial = + w.icon && w.icon.trim() !== '' ? ( + ({ width: theme.sizes.$4, height: theme.sizes.$4, maxWidth: '100%' })} + /> + ) : ( + + ); return ( Date: Wed, 21 Jan 2026 11:37:34 -0500 Subject: [PATCH 02/16] more usages --- ...nInAlternativePhoneCodePhoneNumberCard.tsx | 25 +++++++++++-------- ...artAlternativePhoneCodePhoneNumberCard.tsx | 25 +++++++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx b/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx index f8c3ec42d34..9f105556e40 100644 --- a/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx +++ b/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx @@ -6,7 +6,8 @@ import { Form } from '@/ui/elements/Form'; import { Header } from '@/ui/elements/Header'; import type { FormControlState } from '@/ui/utils/useFormControl'; -import { Button, Col, descriptors, Flex, Image, localizationKeys } from '../../customizables'; +import { ProviderIcon } from '../../common'; +import { Button, Col, descriptors, Flex, localizationKeys } from '../../customizables'; import { CaptchaElement } from '../../elements/CaptchaElement'; import { useEnabledThirdPartyProviders } from '../../hooks'; @@ -32,16 +33,18 @@ export const SignInAlternativePhoneCodePhoneNumberCard = (props: SignUpAlternati showDivider > - {`${strategyToDisplayData[channel].name} ({ - width: theme.sizes.$7, - height: theme.sizes.$7, - maxWidth: '100%', - marginBottom: theme.sizes.$6, - })} - /> + {providerToDisplayData[channel] && ( + ({ + marginBottom: theme.sizes.$6, + })} + /> + )} - {`${strategyToDisplayData[channel].name} ({ - width: theme.sizes.$7, - height: theme.sizes.$7, - maxWidth: '100%', - marginBottom: theme.sizes.$6, - })} - /> + {providerToDisplayData[phoneCodeProvider.channel] && ( + ({ + marginBottom: theme.sizes.$6, + })} + /> + )} Date: Wed, 21 Jan 2026 11:42:10 -0500 Subject: [PATCH 03/16] add changeset --- .changeset/slow-berries-walk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slow-berries-walk.md diff --git a/.changeset/slow-berries-walk.md b/.changeset/slow-berries-walk.md new file mode 100644 index 00000000000..1822560c279 --- /dev/null +++ b/.changeset/slow-berries-walk.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': minor +--- + +Extract `` component to ensure consistency is usage across the UI components. From 55de10b8db762771c350d92b153e49c4add6043f Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 11:58:14 -0500 Subject: [PATCH 04/16] simplify --- packages/ui/src/common/ProviderIcon.tsx | 80 ++++++++++++++++--------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/packages/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx index 71b86ebc977..d906f1133cf 100644 --- a/packages/ui/src/common/ProviderIcon.tsx +++ b/packages/ui/src/common/ProviderIcon.tsx @@ -1,10 +1,37 @@ import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/shared/types'; -import React from 'react'; import { descriptors, Span } from '../customizables'; -import type { PropsOfComponent } from '../styledSystem'; -import { ProviderInitialIcon } from './ProviderInitialIcon'; +import type { InternalTheme, PropsOfComponent } from '../styledSystem'; import { supportsMaskImage } from './providerIconUtils'; +import { ProviderInitialIcon } from './ProviderInitialIcon'; + +const getIconImageStyles = ( + theme: InternalTheme, + id: OAuthProvider | Web3Provider | PhoneCodeChannel, + iconUrl: string, +) => { + if (supportsMaskImage(id)) { + return { + '--cl-icon-fill': theme.colors.$colorForeground, + backgroundColor: 'var(--cl-icon-fill)', + maskImage: `url(${iconUrl})`, + maskSize: 'cover', + maskPosition: 'center', + maskRepeat: 'no-repeat', + }; + } + + return { + backgroundImage: `url(${iconUrl})`, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + }; +}; + +const getThemeSize = (theme: InternalTheme, size: string): string => { + return theme.sizes[size as keyof typeof theme.sizes] || size; +}; export type ProviderIconProps = Omit< PropsOfComponent, @@ -38,6 +65,7 @@ export const ProviderIcon = (props: ProviderIconProps) => { // If no iconUrl or empty string, fallback to ProviderInitialIcon if (!iconUrl || iconUrl.trim() === '') { + const { ref, ...initialIconProps } = rest; return ( { isLoading={isLoading} isDisabled={isDisabled} sx={sx} - {...rest} + {...initialIconProps} /> ); } + // Normalize elementDescriptor to array + const normalizedElementDescriptor = Array.isArray(elementDescriptor) ? elementDescriptor : [elementDescriptor]; + // Use Span with maskImage or backgroundImage based on provider support return ( ({ - display: 'inline-block', - width: theme.sizes[size as keyof typeof theme.sizes] || size, - height: theme.sizes[size as keyof typeof theme.sizes] || size, - maxWidth: '100%', - opacity: isLoading || isDisabled ? 0.5 : 1, - ...(supportsMaskImage(id) - ? { - '--cl-icon-fill': theme.colors.$colorForeground, - backgroundColor: 'var(--cl-icon-fill)', - maskImage: `url(${iconUrl})`, - maskSize: 'cover', - maskPosition: 'center', - maskRepeat: 'no-repeat', - } - : { - backgroundImage: `url(${iconUrl})`, - backgroundSize: 'cover', - backgroundPosition: 'center', - backgroundRepeat: 'no-repeat', - }), - ...(typeof sx === 'function' ? sx(theme) : sx), - })} + sx={theme => { + const iconSize = getThemeSize(theme, size); + return [ + { + display: 'inline-block', + width: iconSize, + height: iconSize, + maxWidth: '100%', + opacity: isLoading || isDisabled ? 0.5 : 1, + ...getIconImageStyles(theme, id, iconUrl), + }, + sx, + ]; + }} {...rest} /> ); From 0ca4f9572981cbeb6c82ae4042e71cd42db33ae4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 12:21:57 -0500 Subject: [PATCH 05/16] simplify --- packages/ui/src/common/ProviderIcon.tsx | 24 +++++------- .../ui/src/common/ProviderInitialIcon.tsx | 4 +- packages/ui/src/common/providerIconUtils.ts | 6 +-- .../UserProfile/ConnectedAccountsSection.tsx | 32 ++++++---------- packages/ui/src/elements/SocialButtons.tsx | 4 +- .../src/elements/Web3SolanaWalletButtons.tsx | 37 ++++++++----------- 6 files changed, 43 insertions(+), 64 deletions(-) diff --git a/packages/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx index d906f1133cf..c810bb400bd 100644 --- a/packages/ui/src/common/ProviderIcon.tsx +++ b/packages/ui/src/common/ProviderIcon.tsx @@ -1,15 +1,14 @@ -import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/shared/types'; +import type { OAuthProvider, PhoneCodeChannel, PhoneCodeProvider, Web3Provider } from '@clerk/shared/types'; import { descriptors, Span } from '../customizables'; +import type { ElementDescriptor, ElementId } from '../customizables/elementDescriptors'; import type { InternalTheme, PropsOfComponent } from '../styledSystem'; import { supportsMaskImage } from './providerIconUtils'; import { ProviderInitialIcon } from './ProviderInitialIcon'; -const getIconImageStyles = ( - theme: InternalTheme, - id: OAuthProvider | Web3Provider | PhoneCodeChannel, - iconUrl: string, -) => { +type ProviderId = OAuthProvider | Web3Provider | PhoneCodeChannel; + +const getIconImageStyles = (theme: InternalTheme, id: ProviderId, iconUrl: string) => { if (supportsMaskImage(id)) { return { '--cl-icon-fill': theme.colors.$colorForeground, @@ -37,15 +36,15 @@ export type ProviderIconProps = Omit< PropsOfComponent, 'elementDescriptor' | 'elementId' | 'aria-label' > & { - id: OAuthProvider | Web3Provider | PhoneCodeChannel; + id: ProviderId; iconUrl?: string | null; name: string; size?: string; isLoading?: boolean; isDisabled?: boolean; alt?: string; - elementDescriptor?: any; - elementId?: any; + elementDescriptor?: ElementDescriptor | Array; + elementId?: ElementId; }; export const ProviderIcon = (props: ProviderIconProps) => { @@ -63,7 +62,6 @@ export const ProviderIcon = (props: ProviderIconProps) => { ...rest } = props; - // If no iconUrl or empty string, fallback to ProviderInitialIcon if (!iconUrl || iconUrl.trim() === '') { const { ref, ...initialIconProps } = rest; return ( @@ -78,13 +76,9 @@ export const ProviderIcon = (props: ProviderIconProps) => { ); } - // Normalize elementDescriptor to array - const normalizedElementDescriptor = Array.isArray(elementDescriptor) ? elementDescriptor : [elementDescriptor]; - - // Use Span with maskImage or backgroundImage based on provider support return ( { diff --git a/packages/ui/src/common/ProviderInitialIcon.tsx b/packages/ui/src/common/ProviderInitialIcon.tsx index a6249ce48fe..148bce4a24e 100644 --- a/packages/ui/src/common/ProviderInitialIcon.tsx +++ b/packages/ui/src/common/ProviderInitialIcon.tsx @@ -4,9 +4,11 @@ import { Box, descriptors, Text } from '../customizables'; import type { PropsOfComponent } from '../styledSystem'; import { common } from '../styledSystem'; +type ProviderId = OAuthProvider | Web3Provider | PhoneCodeProvider; + type ProviderInitialIconProps = PropsOfComponent & { value: string; - id: Web3Provider | OAuthProvider | PhoneCodeProvider; + id: ProviderId; }; export const ProviderInitialIcon = (props: ProviderInitialIconProps) => { diff --git a/packages/ui/src/common/providerIconUtils.ts b/packages/ui/src/common/providerIconUtils.ts index 99a75a6a656..05229015107 100644 --- a/packages/ui/src/common/providerIconUtils.ts +++ b/packages/ui/src/common/providerIconUtils.ts @@ -2,10 +2,8 @@ import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/share export const SUPPORTS_MASK_IMAGE = ['apple', 'github', 'okx_wallet', 'vercel'] as const; -type SupportsMaskImageProvider = (typeof SUPPORTS_MASK_IMAGE)[number]; +type ProviderId = OAuthProvider | Web3Provider | PhoneCodeChannel; -export const supportsMaskImage = ( - id: OAuthProvider | Web3Provider | PhoneCodeChannel, -): id is SupportsMaskImageProvider => { +export const supportsMaskImage = (id: ProviderId): boolean => { return (SUPPORTS_MASK_IMAGE as readonly string[]).includes(id); }; diff --git a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx index 79447792813..be2a5b5b53c 100644 --- a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx +++ b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx @@ -117,11 +117,11 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => }), ); + const { providerToDisplayData } = useEnabledThirdPartyProviders(); + if (!user) { return null; } - - const { providerToDisplayData } = useEnabledThirdPartyProviders(); const label = account.username || account.emailAddress; const fallbackErrorMessage = account.verification?.error?.longMessage; const additionalScopes = findAdditionalScopes(account, additionalOAuthScopes); @@ -153,29 +153,19 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => } }; - const ImageOrInitial = () => { - const providerData = providerToDisplayData[account.provider]; - if (!providerData) { - return null; - } - return ( - ({ flexShrink: 0 })} - /> - ); - }; - return ( ({ overflow: 'hidden', gap: t.space.$2 })}> - + { isLoading={card.loadingMetadata === strategy} isDisabled={card.isLoading} alt={`Sign in with ${strategyToDisplayData[strategy].name}`} - elementDescriptor={[descriptors.providerIcon, descriptors.socialButtonsProviderIcon] as any} - elementId={descriptors.socialButtonsProviderIcon.setId(strategyToDisplayData[strategy].id) as any} + elementDescriptor={[descriptors.providerIcon, descriptors.socialButtonsProviderIcon]} + elementId={descriptors.socialButtonsProviderIcon.setId(strategyToDisplayData[strategy].id)} /> ); diff --git a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx index 9ab2d5d0834..3f24a129b5d 100644 --- a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx +++ b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx @@ -4,7 +4,6 @@ import { MAINNET_ENDPOINT } from '@solana/wallet-standard'; import type { Ref } from 'react'; import React, { forwardRef, isValidElement, useMemo } from 'react'; -import { ProviderIcon } from '@/ui/common/ProviderIcon'; import { WalletInitialIcon } from '@/ui/common/WalletInitialIcon'; import { Button, @@ -12,6 +11,7 @@ import { Flex, Grid, Icon, + Image, localizationKeys, SimpleButton, Spinner, @@ -130,26 +130,21 @@ const Web3SolanaWalletButtonsInner = ({ web3AuthCallback }: Web3WalletButtonsPro ? localizationKeys('web3SolanaWalletButtons.continue', { walletName: w.name }) : w.name; - const imageOrInitial = - w.icon && w.icon.trim() !== '' ? ( - ({ width: theme.sizes.$4, height: theme.sizes.$4, maxWidth: '100%' })} - /> - ) : ( - - ); + const imageOrInitial = w.icon ? ( + {t(localizationKeys('web3SolanaWalletButtons.connect', ({ width: theme.sizes.$4, height: 'auto', maxWidth: '100%' })} + /> + ) : ( + + ); return ( Date: Wed, 21 Jan 2026 12:24:01 -0500 Subject: [PATCH 06/16] cleanup --- packages/ui/src/common/ProviderIcon.tsx | 2 +- .../UserProfile/ConnectedAccountsSection.tsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx index c810bb400bd..89d4ce4ea05 100644 --- a/packages/ui/src/common/ProviderIcon.tsx +++ b/packages/ui/src/common/ProviderIcon.tsx @@ -1,4 +1,4 @@ -import type { OAuthProvider, PhoneCodeChannel, PhoneCodeProvider, Web3Provider } from '@clerk/shared/types'; +import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/shared/types'; import { descriptors, Span } from '../customizables'; import type { ElementDescriptor, ElementId } from '../customizables/elementDescriptors'; diff --git a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx index be2a5b5b53c..ff00fdb0f25 100644 --- a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx +++ b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx @@ -153,15 +153,17 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => } }; + const providerData = providerToDisplayData[account.provider]; + return ( ({ overflow: 'hidden', gap: t.space.$2 })}> center > ({ color: t.colors.$colorForeground })}>{`${ - providerToDisplayData[account.provider].name + providerData?.name || account.provider }`} Date: Wed, 21 Jan 2026 12:25:37 -0500 Subject: [PATCH 07/16] Update ProviderInitialIcon.tsx --- packages/ui/src/common/ProviderInitialIcon.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/common/ProviderInitialIcon.tsx b/packages/ui/src/common/ProviderInitialIcon.tsx index 148bce4a24e..a6249ce48fe 100644 --- a/packages/ui/src/common/ProviderInitialIcon.tsx +++ b/packages/ui/src/common/ProviderInitialIcon.tsx @@ -4,11 +4,9 @@ import { Box, descriptors, Text } from '../customizables'; import type { PropsOfComponent } from '../styledSystem'; import { common } from '../styledSystem'; -type ProviderId = OAuthProvider | Web3Provider | PhoneCodeProvider; - type ProviderInitialIconProps = PropsOfComponent & { value: string; - id: ProviderId; + id: Web3Provider | OAuthProvider | PhoneCodeProvider; }; export const ProviderInitialIcon = (props: ProviderInitialIconProps) => { From 80c06da0383c4d1cd845cb2fc3ea4fa43f98bbf4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 12:26:54 -0500 Subject: [PATCH 08/16] use inline --- .../UserProfile/ConnectedAccountsMenu.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx b/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx index fdc467e22a0..deaa2c31de5 100644 --- a/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx +++ b/packages/ui/src/components/UserProfile/ConnectedAccountsMenu.tsx @@ -36,19 +36,6 @@ const ConnectMenuButton = (props: { strategy: OAuthStrategy; onClick?: () => voi }); }); - const imageOrInitial = ( - - ); - const connect = () => { if (!user) { return; @@ -87,7 +74,18 @@ const ConnectMenuButton = (props: { strategy: OAuthStrategy; onClick?: () => voi justifyContent: 'start', gap: t.space.$2, })} - leftIcon={imageOrInitial} + leftIcon={ + + } /> ); }; From 5095c93bc2bcf2aafe00294ed2f78c9cf9e8f001 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 12:29:13 -0500 Subject: [PATCH 09/16] size should be consistent --- packages/ui/src/components/UserProfile/Web3Form.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/components/UserProfile/Web3Form.tsx b/packages/ui/src/components/UserProfile/Web3Form.tsx index b5cf65df9b8..19c4b320c7a 100644 --- a/packages/ui/src/components/UserProfile/Web3Form.tsx +++ b/packages/ui/src/components/UserProfile/Web3Form.tsx @@ -99,7 +99,6 @@ export const AddWeb3WalletActionMenu = () => { isLoading={card.loadingMetadata === strategy} isDisabled={card.isLoading} alt={`Connect ${strategyToDisplayData[strategy].name}`} - size='$5' elementDescriptor={descriptors.providerIcon} elementId={descriptors.providerIcon.setId(strategyToDisplayData[strategy].id)} /> From ddb95cd70e7f205d0ad67cd97312231a3058fe6c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 12:30:42 -0500 Subject: [PATCH 10/16] Update Web3SolanaWalletButtons.tsx --- packages/ui/src/elements/Web3SolanaWalletButtons.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx index 3f24a129b5d..cfa88a47b0b 100644 --- a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx +++ b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx @@ -134,6 +134,7 @@ const Web3SolanaWalletButtonsInner = ({ web3AuthCallback }: Web3WalletButtonsPro {t(localizationKeys('web3SolanaWalletButtons.connect', ({ width: theme.sizes.$4, height: 'auto', maxWidth: '100%' })} From c2022d4bf1931de2cfc3b004e40eccdd75849399 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 12:33:02 -0500 Subject: [PATCH 11/16] simplify --- packages/ui/src/common/ProviderIcon.tsx | 7 ++++++- packages/ui/src/common/providerIconUtils.ts | 9 --------- 2 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 packages/ui/src/common/providerIconUtils.ts diff --git a/packages/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx index 89d4ce4ea05..95d3359f4e8 100644 --- a/packages/ui/src/common/ProviderIcon.tsx +++ b/packages/ui/src/common/ProviderIcon.tsx @@ -3,11 +3,16 @@ import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/share import { descriptors, Span } from '../customizables'; import type { ElementDescriptor, ElementId } from '../customizables/elementDescriptors'; import type { InternalTheme, PropsOfComponent } from '../styledSystem'; -import { supportsMaskImage } from './providerIconUtils'; import { ProviderInitialIcon } from './ProviderInitialIcon'; type ProviderId = OAuthProvider | Web3Provider | PhoneCodeChannel; +const SUPPORTS_MASK_IMAGE = ['apple', 'github', 'okx_wallet', 'vercel'] as const; + +const supportsMaskImage = (id: ProviderId): boolean => { + return (SUPPORTS_MASK_IMAGE as readonly string[]).includes(id); +}; + const getIconImageStyles = (theme: InternalTheme, id: ProviderId, iconUrl: string) => { if (supportsMaskImage(id)) { return { diff --git a/packages/ui/src/common/providerIconUtils.ts b/packages/ui/src/common/providerIconUtils.ts deleted file mode 100644 index 05229015107..00000000000 --- a/packages/ui/src/common/providerIconUtils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { OAuthProvider, PhoneCodeChannel, Web3Provider } from '@clerk/shared/types'; - -export const SUPPORTS_MASK_IMAGE = ['apple', 'github', 'okx_wallet', 'vercel'] as const; - -type ProviderId = OAuthProvider | Web3Provider | PhoneCodeChannel; - -export const supportsMaskImage = (id: ProviderId): boolean => { - return (SUPPORTS_MASK_IMAGE as readonly string[]).includes(id); -}; From ad0aff8d6ca32335c16f61144972726f197fa2ad Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 12:37:18 -0500 Subject: [PATCH 12/16] handle fallbacks --- .../SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx | 5 +++-- .../SignUpStartAlternativePhoneCodePhoneNumberCard.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx b/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx index 9f105556e40..5c784ba612b 100644 --- a/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx +++ b/packages/ui/src/components/SignIn/SignInAlternativePhoneCodePhoneNumberCard.tsx @@ -24,6 +24,7 @@ export const SignInAlternativePhoneCodePhoneNumberCard = (props: SignUpAlternati const provider = phoneCodeProvider.name; const channel = phoneCodeProvider.channel; const card = useCardState(); + const strategyData = strategyToDisplayData[channel]; return ( @@ -37,8 +38,8 @@ export const SignInAlternativePhoneCodePhoneNumberCard = (props: SignUpAlternati ({ marginBottom: theme.sizes.$6, diff --git a/packages/ui/src/components/SignUp/SignUpStartAlternativePhoneCodePhoneNumberCard.tsx b/packages/ui/src/components/SignUp/SignUpStartAlternativePhoneCodePhoneNumberCard.tsx index 62aa81ce1b4..3309b63439d 100644 --- a/packages/ui/src/components/SignUp/SignUpStartAlternativePhoneCodePhoneNumberCard.tsx +++ b/packages/ui/src/components/SignUp/SignUpStartAlternativePhoneCodePhoneNumberCard.tsx @@ -27,6 +27,7 @@ export const SignUpStartAlternativePhoneCodePhoneNumberCard = (props: SignUpForm const provider = phoneCodeProvider.name; const channel = phoneCodeProvider.channel; const card = useCardState(); + const strategyData = strategyToDisplayData[channel]; const shouldShow = (name: keyof typeof fields) => { return !!fields[name] && fields[name]?.required; @@ -44,8 +45,8 @@ export const SignUpStartAlternativePhoneCodePhoneNumberCard = (props: SignUpForm ({ marginBottom: theme.sizes.$6, From 84ddb9e7498647bdff102c6bea9aa6cf68ad1327 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 12:43:24 -0500 Subject: [PATCH 13/16] handle tests --- .../common/__tests__/ProviderIcon.test.tsx | 432 ++++++++++++++++++ .../EnterpriseAccountsSection.test.tsx | 18 +- 2 files changed, 441 insertions(+), 9 deletions(-) create mode 100644 packages/ui/src/common/__tests__/ProviderIcon.test.tsx diff --git a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx new file mode 100644 index 00000000000..2e7aebdbc11 --- /dev/null +++ b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx @@ -0,0 +1,432 @@ +import { describe, expect, it } from 'vitest'; + +import { bindCreateFixtures } from '@/test/create-fixtures'; +import { render, screen } from '@/test/utils'; + +import { ProviderIcon } from '../ProviderIcon'; + +const { createFixtures } = bindCreateFixtures('SignIn'); + +describe('ProviderIcon', () => { + describe('Rendering with iconUrl', () => { + it('renders Span with correct aria-label when iconUrl is provided', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + expect(icon.tagName).toBe('SPAN'); + }); + + it('uses custom alt text when provided', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Custom Google logo'); + expect(icon).toBeInTheDocument(); + }); + + it('applies mask-image styles for supported providers (apple)', async () => { + const { wrapper } = await createFixtures(); + + const { container } = render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Apple icon'); + const styles = window.getComputedStyle(icon); + + // Check that mask-image is applied (via inline styles) + expect(icon).toHaveStyle({ + display: 'inline-block', + }); + }); + + it('applies mask-image styles for supported providers (github)', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('GitHub icon'); + expect(icon).toBeInTheDocument(); + }); + + it('applies mask-image styles for supported providers (okx_wallet)', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('OKX Wallet icon'); + expect(icon).toBeInTheDocument(); + }); + + it('applies mask-image styles for supported providers (vercel)', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Vercel icon'); + expect(icon).toBeInTheDocument(); + }); + + it('applies background-image styles for non-mask-image providers', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + }); + }); + + describe('Rendering without iconUrl', () => { + it('falls back to ProviderInitialIcon when iconUrl is null', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + // ProviderInitialIcon renders the first letter of the name + const initial = screen.getByText('G'); + expect(initial).toBeInTheDocument(); + }); + + it('falls back to ProviderInitialIcon when iconUrl is undefined', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const initial = screen.getByText('G'); + expect(initial).toBeInTheDocument(); + }); + + it('falls back to ProviderInitialIcon when iconUrl is empty string', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const initial = screen.getByText('A'); + expect(initial).toBeInTheDocument(); + }); + + it('falls back to ProviderInitialIcon when iconUrl is whitespace-only', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const initial = screen.getByText('G'); + expect(initial).toBeInTheDocument(); + }); + + it('passes isLoading prop to ProviderInitialIcon', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const initial = screen.getByText('G'); + expect(initial).toBeInTheDocument(); + }); + + it('passes isDisabled prop to ProviderInitialIcon', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const initial = screen.getByText('G'); + expect(initial).toBeInTheDocument(); + }); + }); + + describe('Loading and disabled states', () => { + it('applies opacity 0.5 when isLoading is true', async () => { + const { wrapper } = await createFixtures(); + + const { container } = render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + const styles = window.getComputedStyle(icon); + expect(styles.opacity).toBe('0.5'); + }); + + it('applies opacity 0.5 when isDisabled is true', async () => { + const { wrapper } = await createFixtures(); + + const { container } = render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + const styles = window.getComputedStyle(icon); + expect(styles.opacity).toBe('0.5'); + }); + + it('applies opacity 1 when neither isLoading nor isDisabled is true', async () => { + const { wrapper } = await createFixtures(); + + const { container } = render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + const styles = window.getComputedStyle(icon); + expect(styles.opacity).toBe('1'); + }); + }); + + describe('Size prop', () => { + it('uses default size $4 when not provided', async () => { + const { wrapper } = await createFixtures(); + + const { container } = render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + // Size is applied via theme, so we verify the element exists + // The actual size value depends on theme configuration + }); + + it('uses custom size when provided', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + }); + }); + + describe('Accessibility', () => { + it('sets aria-label from alt prop when provided', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google provider icon'); + expect(icon).toHaveAttribute('aria-label', 'Google provider icon'); + }); + + it('generates aria-label from name when alt is not provided', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toHaveAttribute('aria-label', 'Google icon'); + }); + + it('uses correct elementDescriptor', async () => { + const { wrapper } = await createFixtures(); + + const { container } = render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + // Element descriptor is applied via data attributes in the styled system + }); + }); + + describe('Edge cases', () => { + it('handles providers with different casing', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + }); + + it('handles custom elementDescriptor', async () => { + const { wrapper } = await createFixtures(); + const { descriptors } = await import('../../customizables'); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + }); + + it('handles custom elementId', async () => { + const { wrapper } = await createFixtures(); + const { descriptors } = await import('../../customizables'); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('Google icon'); + expect(icon).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/ui/src/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx b/packages/ui/src/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx index 9ee0ee40d0e..cabd738fa30 100644 --- a/packages/ui/src/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx +++ b/packages/ui/src/components/UserProfile/__tests__/EnterpriseAccountsSection.test.tsx @@ -243,12 +243,12 @@ describe('EnterpriseAccountsSection ', () => { it('renders connection', async () => { const { wrapper } = await createFixtures(withOAuthBuiltInEnterpriseConnection); - const { getByText, getByRole } = render(, { wrapper }); + const { getByText, getByLabelText } = render(, { wrapper }); getByText(/^Enterprise accounts/i); getByText(/google/i); - const img = getByRole('img', { name: /google/i }); - expect(img.getAttribute('src')).toBe('https://img.clerk.com/static/google.svg?width=160'); + const icon = getByLabelText(/google's icon/i); + expect(icon).toBeInTheDocument(); getByText(/test@clerk.com/i); }); }); @@ -260,12 +260,12 @@ describe('EnterpriseAccountsSection ', () => { const { wrapper } = await createFixtures(withOAuthCustomEnterpriseConnection(mockLogoUrl)); - const { getByText, getByRole } = render(, { wrapper }); + const { getByText, getByLabelText } = render(, { wrapper }); getByText(/^Enterprise accounts/i); getByText(/roblox/i); - const img = getByRole('img', { name: /roblox/i }); - expect(img.getAttribute('src')).toContain(mockLogoUrl); + const icon = getByLabelText(/roblox's icon/i); + expect(icon).toBeInTheDocument(); getByText(/test@clerk.com/i); }); }); @@ -288,12 +288,12 @@ describe('EnterpriseAccountsSection ', () => { it('renders connection', async () => { const { wrapper } = await createFixtures(withSamlEnterpriseConnection); - const { getByText, getByRole } = render(, { wrapper }); + const { getByText, getByLabelText } = render(, { wrapper }); getByText(/^Enterprise accounts/i); getByText(/okta workforce/i); - const img = getByRole('img', { name: /okta/i }); - expect(img.getAttribute('src')).toBe('https://img.clerk.com/static/okta.svg?width=160'); + const icon = getByLabelText(/okta workforce's icon/i); + expect(icon).toBeInTheDocument(); getByText(/test@clerk.com/i); }); }); From 0c6152114dea0b397d13dabfe95a92bde946f5d4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 13:03:29 -0500 Subject: [PATCH 14/16] cleanup --- packages/clerk-js/sandbox/app.ts | 3 ++- .../ui/src/common/__tests__/ProviderIcon.test.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 315e0822f31..64498cd031d 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -1,4 +1,5 @@ -import { PageMocking, type MockScenario } from '@clerk/msw'; +import { type MockScenario, PageMocking } from '@clerk/msw'; + import * as l from '../../localizations'; import type { Clerk as ClerkType } from '../'; import * as scenarios from './scenarios'; diff --git a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx index 2e7aebdbc11..48657e10a83 100644 --- a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx +++ b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx @@ -46,7 +46,7 @@ describe('ProviderIcon', () => { it('applies mask-image styles for supported providers (apple)', async () => { const { wrapper } = await createFixtures(); - const { container } = render( + render( { it('applies opacity 0.5 when isLoading is true', async () => { const { wrapper } = await createFixtures(); - const { container } = render( + render( { it('applies opacity 0.5 when isDisabled is true', async () => { const { wrapper } = await createFixtures(); - const { container } = render( + render( { it('applies opacity 1 when neither isLoading nor isDisabled is true', async () => { const { wrapper } = await createFixtures(); - const { container } = render( + render( { it('uses default size $4 when not provided', async () => { const { wrapper } = await createFixtures(); - const { container } = render( + render( { it('uses correct elementDescriptor', async () => { const { wrapper } = await createFixtures(); - const { container } = render( + render( Date: Wed, 21 Jan 2026 13:09:28 -0500 Subject: [PATCH 15/16] Apply suggestion from @alexcarpenter --- packages/clerk-js/sandbox/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 64498cd031d..392b25aa034 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -1,4 +1,4 @@ -import { type MockScenario, PageMocking } from '@clerk/msw'; +import { PageMocking, type MockScenario } from '@clerk/msw'; import * as l from '../../localizations'; import type { Clerk as ClerkType } from '../'; From fe0801e1dbc33fc378b774bc3aeccfbd9db2969c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 21 Jan 2026 13:09:44 -0500 Subject: [PATCH 16/16] Apply suggestion from @alexcarpenter --- packages/clerk-js/sandbox/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 392b25aa034..315e0822f31 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -1,5 +1,4 @@ import { PageMocking, type MockScenario } from '@clerk/msw'; - import * as l from '../../localizations'; import type { Clerk as ClerkType } from '../'; import * as scenarios from './scenarios';