diff --git a/.changeset/bordered-security-icons.md b/.changeset/bordered-security-icons.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/bordered-security-icons.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/AGENTS.md b/packages/mosaic/AGENTS.md index 1936031f567..127bb615991 100644 --- a/packages/mosaic/AGENTS.md +++ b/packages/mosaic/AGENTS.md @@ -47,6 +47,7 @@ Most problems a feature hits already have a shared answer. Find the row for your | A spinner that should not flash or flicker | `useSpinDelay` (`src/hooks/`) | | A placeholder for content that is still loading | `Skeleton` (`src/components/skeleton/`), see [Skeletons](docs/skeletons.md) | | A date shown to the user | `formatDate` (`src/utils/format-date.ts`) | +| A value turned into kebab case, e.g. for a `data-*` attribute | `kebabCase` (`src/utils/kebab-case.ts`) | | Current time for display labels | `useNow` + `MosaicProvider` ([#10098](https://github.com/clerk/javascript/pull/10098), once available) | | Whether enterprise SSO permits adding another identification | `allowsIdentificationCreation` (`src/utils/allows-identification-creation.ts`) | | An inline message row that animates open and closed | `useHeldMessage`, `useMessageHeight`, `FeedbackBody` (`src/utils/feedback.tsx`) + `styles/feedback.styles.ts` | diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-provider-media.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-provider-media.view.test.tsx new file mode 100644 index 00000000000..a9a14b66ef0 --- /dev/null +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-provider-media.view.test.tsx @@ -0,0 +1,22 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { UserProfileProviderMedia } from '../user-profile-provider-media'; + +describe('UserProfileProviderMedia', () => { + it.each([ + ['an image logo', { iconUrl: 'https://img.clerk.com/static/okx_wallet.svg' }], + ['a masked logo', { iconUrl: 'https://img.clerk.com/static/okx_wallet.svg', monochromeIcon: true }], + ['the initial fallback', {}], + ])('marks %s with the kebab-cased provider id', (_, props) => { + const { container } = render( + , + ); + + expect(container.querySelector('.cl-provider-icon')).toHaveAttribute('data-provider', 'okx-wallet'); + }); +}); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx index 9e2f5df2392..cd82e7776e2 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx @@ -36,6 +36,7 @@ export function UserProfileConnectProviderRowView({ @@ -104,6 +105,7 @@ export function UserProfileConnectedAccountRowView({ diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts index 00d2f9b60d9..e0cc0b7249d 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts @@ -88,6 +88,7 @@ export function createProviderCatalog( enabled: enabled.has(strategy), display: { provider: name, + iconId: provider, iconUrl: iconImageUrl(provider), monochromeIcon: isMonochromeProvider(provider), }, @@ -109,7 +110,7 @@ export function createProviderCatalog( strategy, provider, enabled: enabled.has(strategy), - display: { provider: settings?.name || provider, iconUrl: settings?.logo_url || undefined }, + display: { provider: settings?.name || provider, iconId: provider, iconUrl: settings?.logo_url || undefined }, }); } return [ diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts index 77168d0dfd5..bfd25079a0b 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts @@ -8,6 +8,7 @@ export type ConnectedAccountActionResult = 'redirecting' | void; export interface ConnectedAccountProviderDisplay { provider: string; + iconId?: string; iconUrl?: string; monochromeIcon?: boolean; } diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx index 28f0c576cbc..51415a3af39 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-mfa.view.tsx @@ -44,7 +44,10 @@ export function UserProfileAddMfaView({ methods, onSelect }: UserProfileAddMfaVi } > - + diff --git a/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx b/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx index 01eb9cd0498..0fbbef9c7d4 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-provider-icon.tsx @@ -10,7 +10,10 @@ type UserProfileProviderIconProps = { iconUrl: string; glyph?: never } | { iconU export function UserProfileProviderIcon(props: UserProfileProviderIconProps) { return ( - + {'iconUrl' in props ? ( ({ maskImage: `url(${JSON.stringify(url)})` }), fallback: { @@ -22,7 +25,7 @@ export const styles = stylex.create({ display: 'inline-flex', fontSize: '0.625rem', justifyContent: 'center', - height: space['6'], - width: space['6'], + height: space['5'], + width: space['5'], }, }); diff --git a/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx b/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx index 52b3c75b331..429b3191644 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-provider-media.tsx @@ -2,37 +2,46 @@ import * as stylex from '@stylexjs/stylex'; import { IconFrame } from '../../components/icon'; import { Section } from '../../components/section'; +import { mergeStyleProps, themeProps } from '../../props'; +import { kebabCase } from '../../utils/kebab-case'; import { styles } from './user-profile-provider-media.styles'; export function UserProfileProviderMedia({ provider, + iconId, iconUrl, monochromeIcon, }: { provider: string; + iconId?: string; iconUrl?: string; monochromeIcon?: boolean; }) { const url = iconUrl?.trim(); + const slot = themeProps('provider-icon', { provider: iconId && kebabCase(iconId) }); + const offset = iconId === 'apple' && styles.appleOffset; return ( - + {url && monochromeIcon ? ( ) : url ? ( ) : ( {provider.trim().charAt(0).toUpperCase()} diff --git a/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx b/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx index 7de7823e8cf..af73a0516c8 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-security-icon.tsx @@ -1,4 +1,4 @@ -import { Icon } from '../../components/icon'; +import { Icon, IconFrame } from '../../components/icon'; import { Section } from '../../components/section'; import { DeviceLaptop } from '../../icons/glyphs/device-laptop'; import { DevicePhone } from '../../icons/glyphs/device-phone'; @@ -20,15 +20,17 @@ const icons = { export function UserProfileSecurityIcon({ name }: { name: UserProfileSecurityIconName }) { return ( - - + + + + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts index 4038ea7bb12..db674839f5a 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, radiusVars, space } from '../../tokens.stylex'; +import { colorVars, space } from '../../tokens.stylex'; export const styles = stylex.create({ icon: { @@ -9,13 +9,6 @@ export const styles = stylex.create({ height: space['4.5'], width: space['4.5'], }, - media: { - borderColor: colorVars['--cl-color-border-subtle'], - borderRadius: radiusVars['--cl-radius-md'], - borderStyle: 'solid', - borderWidth: '1px', - backgroundColor: colorVars['--cl-color-background-subtle'], - }, sectionCards: { gap: space['3'], display: 'flex', diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx index 1f94832d3ac..e788761c532 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallet-row.view.tsx @@ -55,6 +55,7 @@ export function UserProfileWeb3WalletRowView({ {wallet.provider ? ( diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts index 21faca20f15..9da99dda1c5 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.model.ts @@ -80,6 +80,7 @@ export function projectWeb3Wallets({ allowCreation && wallet.verification.strategy !== null && enabledStrategies.includes(wallet.verification.strategy), + iconId: provider?.provider, iconUrl: provider ? iconImageUrl(provider.provider) : undefined, monochromeIcon: provider ? isMonochromeProvider(provider.provider) : false, isPrimary: wallet.id === primaryId, @@ -93,6 +94,7 @@ export function projectWeb3Wallets({ id: provider.strategy, walletPicker: provider.provider === 'solana' ? 'solana' : undefined, provider: provider.name, + iconId: provider.provider, iconUrl: iconImageUrl(provider.provider), monochromeIcon: isMonochromeProvider(provider.provider), })) diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts index 0b0d9666a4c..a0ce8fec689 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.types.ts @@ -9,6 +9,7 @@ export interface UserProfileWeb3Provider { id: Web3Strategy; walletPicker?: 'solana'; provider: string; + iconId?: string; iconUrl?: string; monochromeIcon?: boolean; connectError?: string; @@ -21,6 +22,7 @@ export interface UserProfileWeb3Wallet { walletPicker?: 'solana'; canVerify?: boolean; provider?: string; + iconId?: string; iconUrl?: string; monochromeIcon?: boolean; isPrimary?: boolean; diff --git a/packages/mosaic/src/props.ts b/packages/mosaic/src/props.ts index 97abd70c7de..d12f2aabeaf 100644 --- a/packages/mosaic/src/props.ts +++ b/packages/mosaic/src/props.ts @@ -2,6 +2,7 @@ import type * as stylex from '@stylexjs/stylex'; import type React from 'react'; import type { ComponentProps } from './primitives/utils'; +import { kebabCase } from './utils/kebab-case'; /** * What `xstyle` accepts: exactly what `stylex.props(...)` does, since that is where a part @@ -63,8 +64,6 @@ export type MosaicComponentProps type VariantValue = string | number | boolean | null | undefined; -const kebab = (name: string) => name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); - /** * The stable class + variant reflection for a slot. * @@ -84,7 +83,7 @@ export function themeProps( if (value == null || value === false) { continue; } - attrs[`data-${kebab(axis)}`] = value === true ? '' : String(value); + attrs[`data-${kebabCase(axis)}`] = value === true ? '' : String(value); } return { className: `cl-${slot}`, ...attrs }; } diff --git a/packages/mosaic/src/utils/__tests__/kebab-case.test.ts b/packages/mosaic/src/utils/__tests__/kebab-case.test.ts new file mode 100644 index 00000000000..d9fb6194d2f --- /dev/null +++ b/packages/mosaic/src/utils/__tests__/kebab-case.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { kebabCase } from '../kebab-case'; + +describe('kebabCase', () => { + it.each([ + ['okx_wallet', 'okx-wallet'], + ['colorScheme', 'color-scheme'], + ['HTTPRequest', 'http-request'], + ['some whitespace', 'some-whitespace'], + ['already-kebab', 'already-kebab'], + ['apple', 'apple'], + ['', ''], + ])('converts %j to %j', (input, expected) => { + expect(kebabCase(input)).toBe(expected); + }); +}); diff --git a/packages/mosaic/src/utils/kebab-case.ts b/packages/mosaic/src/utils/kebab-case.ts new file mode 100644 index 00000000000..bc72432a214 --- /dev/null +++ b/packages/mosaic/src/utils/kebab-case.ts @@ -0,0 +1,12 @@ +export function kebabCase(value: string): string { + return words(value) + .map(word => word.toLowerCase()) + .join('-'); +} + +const CASE_SPLIT_PATTERN = + /\p{Lu}?\p{Ll}+|[0-9]+|\p{Lu}+(?!\p{Ll})|\p{Emoji_Presentation}|\p{Extended_Pictographic}|\p{L}+/gu; + +function words(value: string): string[] { + return Array.from(value.match(CASE_SPLIT_PATTERN) ?? []); +} diff --git a/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts index b0070f08823..28951126ece 100644 --- a/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts +++ b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts @@ -13,13 +13,20 @@ import { idleReverification } from './reverification'; export const connectedAccount: UserProfileConnectedAccount = { id: 'google', provider: 'Google', + iconId: 'google', identifier: 'test@example.com', iconUrl: 'https://img.clerk.com/static/google.svg', status: 'connected', }; const connectionProviders: UserProfileConnectionProvider[] = [ - { id: 'google', provider: 'Google', iconUrl: 'https://img.clerk.com/static/google.svg' }, - { id: 'apple', provider: 'Apple', iconUrl: 'https://img.clerk.com/static/apple.svg', monochromeIcon: true }, + { id: 'google', provider: 'Google', iconId: 'google', iconUrl: 'https://img.clerk.com/static/google.svg' }, + { + id: 'apple', + provider: 'Apple', + iconId: 'apple', + iconUrl: 'https://img.clerk.com/static/apple.svg', + monochromeIcon: true, + }, ]; export function useConnectedAccountsFixture({ @@ -60,6 +67,7 @@ export function useConnectedAccountsFixture({ { id: provider.id, provider: provider.provider, + iconId: provider.iconId, iconUrl: provider.iconUrl, monochromeIcon: provider.monochromeIcon, identifier: 'test@example.com', diff --git a/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts b/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts index 4bd6ba7260f..8fd97ba984c 100644 --- a/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts +++ b/packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts @@ -12,10 +12,16 @@ import { chaosText } from '@/lib/chaos'; import { idleReverification } from './reverification'; const providers: UserProfileWeb3Provider[] = [ - { id: 'web3_metamask_signature', provider: 'MetaMask', iconUrl: 'https://img.clerk.com/static/metamask.svg' }, + { + id: 'web3_metamask_signature', + provider: 'MetaMask', + iconId: 'metamask', + iconUrl: 'https://img.clerk.com/static/metamask.svg', + }, { id: 'web3_coinbase_wallet_signature', provider: 'Coinbase Wallet', + iconId: 'coinbase_wallet', iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg', }, ]; @@ -24,6 +30,7 @@ export const primaryWallet: UserProfileWeb3Wallet = { id: 'wallet_1', providerId: 'web3_metamask_signature', provider: 'MetaMask', + iconId: 'metamask', iconUrl: 'https://img.clerk.com/static/metamask.svg', address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F', isPrimary: true, @@ -34,6 +41,7 @@ export const secondaryWallet: UserProfileWeb3Wallet = { id: 'wallet_2', providerId: 'web3_coinbase_wallet_signature', provider: 'Coinbase Wallet', + iconId: 'coinbase_wallet', iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg', address: '0x1234567890abcdef1234567890abcdef12345678', isVerified: true, @@ -87,6 +95,7 @@ export function useWeb3WalletsFixture({ ...current, { provider: provider.provider, + iconId: provider.iconId, iconUrl: provider.iconUrl, id: `wallet_${id}`, providerId: id, diff --git a/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx index a51b328ee87..344d0e27ad4 100644 --- a/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx +++ b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx @@ -46,6 +46,7 @@ export function ConnectionError() { { id: 'apple', provider: 'Apple', + iconId: 'apple', iconUrl: 'https://img.clerk.com/static/apple.svg', monochromeIcon: true, connectError: 'Unable to connect. Please try again.', diff --git a/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx b/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx index 8420f89a827..6e1fad77714 100644 --- a/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx +++ b/packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx @@ -35,7 +35,9 @@ export function ConnectOnly() { export function UnverifiedWallet() { const fixture = useWeb3WalletsFixture({ initialWallets: [{ ...primaryWallet, isPrimary: false, isVerified: false }], - availableProviders: [{ id: 'web3_metamask_signature', provider: 'MetaMask', iconUrl: primaryWallet.iconUrl }], + availableProviders: [ + { id: 'web3_metamask_signature', provider: 'MetaMask', iconId: 'metamask', iconUrl: primaryWallet.iconUrl }, + ], }); return (