diff --git a/.changeset/agentid-oauth-provider.md b/.changeset/agentid-oauth-provider.md new file mode 100644 index 00000000000..ed835e6bb48 --- /dev/null +++ b/.changeset/agentid-oauth-provider.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': minor +'@clerk/ui': patch +--- + +Add `agentid` to `OAuthProvider` and `OAUTH_PROVIDERS` to support the "Continue with AgentID" OAuth flow. Instances with the connection enabled now render a "Continue with AgentID" button, with the AgentID mark tinted to match the theme's foreground color so it stays legible in dark mode. diff --git a/packages/shared/src/oauth.ts b/packages/shared/src/oauth.ts index 68360d5ae46..b6b86050b83 100644 --- a/packages/shared/src/oauth.ts +++ b/packages/shared/src/oauth.ts @@ -174,6 +174,12 @@ export const OAUTH_PROVIDERS: OAuthProviderData[] = [ name: 'Vercel', docsUrl: 'https://clerk.com/docs/authentication/social-connections/vercel', }, + { + provider: 'agentid', + strategy: 'oauth_agentid', + name: 'AgentID', + docsUrl: 'https://clerk.com/docs/guides/configure/auth-strategies/social-connections/agentid', + }, ]; interface getOAuthProviderDataProps { diff --git a/packages/shared/src/types/oauth.ts b/packages/shared/src/types/oauth.ts index 86854dffd19..d6ebc366323 100644 --- a/packages/shared/src/types/oauth.ts +++ b/packages/shared/src/types/oauth.ts @@ -68,6 +68,8 @@ export type HuggingfaceOAuthProvider = 'huggingface'; /** @inline */ export type VercelOauthProvider = 'vercel'; /** @inline */ +export type AgentIDOauthProvider = 'agentid'; +/** @inline */ export type CustomOauthProvider = `custom_${string}`; /** Represents the available OAuth providers. */ @@ -101,4 +103,5 @@ export type OAuthProvider = | EnstallOauthProvider | HuggingfaceOAuthProvider | VercelOauthProvider + | AgentIDOauthProvider | CustomOauthProvider; diff --git a/packages/shared/src/types/runtime-values.ts b/packages/shared/src/types/runtime-values.ts index fbbfc413c8a..93844a29f93 100644 --- a/packages/shared/src/types/runtime-values.ts +++ b/packages/shared/src/types/runtime-values.ts @@ -221,6 +221,18 @@ export const OAUTH_PROVIDERS: OAuthProviderData[] = [ name: 'Hugging Face', docsUrl: 'https://clerk.com/docs/authentication/social-connections/huggingface', }, + { + provider: 'vercel', + strategy: 'oauth_vercel', + name: 'Vercel', + docsUrl: 'https://clerk.com/docs/authentication/social-connections/vercel', + }, + { + provider: 'agentid', + strategy: 'oauth_agentid', + name: 'AgentID', + docsUrl: 'https://clerk.com/docs/guides/configure/auth-strategies/social-connections/agentid', + }, ]; /** diff --git a/packages/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx index 0a2a6d4b425..e16b30a5f7c 100644 --- a/packages/ui/src/common/ProviderIcon.tsx +++ b/packages/ui/src/common/ProviderIcon.tsx @@ -7,7 +7,7 @@ import { ProviderInitialIcon } from './ProviderInitialIcon'; type ProviderId = OAuthProvider | Web3Provider | PhoneCodeChannel; -const SUPPORTS_MASK_IMAGE = ['apple', 'github', 'okx_wallet', 'vercel', 'x'] as const; +export const SUPPORTS_MASK_IMAGE = ['agentid', 'apple', 'github', 'okx_wallet', 'vercel', 'x'] as const; const supportsMaskImage = (id: ProviderId): boolean => { return (SUPPORTS_MASK_IMAGE as readonly string[]).includes(id); diff --git a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx index 370016475e2..10c8ecddc71 100644 --- a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx +++ b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; import { render, screen } from '@/test/utils'; -import { ProviderIcon } from '../ProviderIcon'; +import { ProviderIcon, SUPPORTS_MASK_IMAGE } from '../ProviderIcon'; const { createFixtures } = bindCreateFixtures('SignIn'); @@ -43,94 +43,29 @@ describe('ProviderIcon', () => { expect(icon).toBeInTheDocument(); }); - it('applies mask-image styles for supported providers (apple)', async () => { - const { wrapper } = await createFixtures(); - - render( - , - { wrapper }, - ); - - const icon = screen.getByLabelText('Apple 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('has providers to cover, so the mask-image cases below are not vacuous', () => { + expect(SUPPORTS_MASK_IMAGE.length).toBeGreaterThan(0); }); - it('applies mask-image styles for supported providers (okx_wallet)', async () => { + it.each(SUPPORTS_MASK_IMAGE)('tints supported provider %s with the foreground color via mask-image', async id => { const { wrapper } = await createFixtures(); + const iconUrl = `https://example.com/${id}-icon.svg`; render( , { wrapper }, ); - const icon = screen.getByLabelText('OKX Wallet icon'); + const icon = screen.getByLabelText(`${id} icon`); expect(icon).toBeInTheDocument(); - }); - - it('applies mask-image styles for supported providers (x)', async () => { - const { wrapper } = await createFixtures(); - - render( - , - { wrapper }, - ); - const icon = screen.getByLabelText('X / Twitter icon'); - expect(icon).toBeInTheDocument(); - - // The mask-image path tints the icon with the foreground color so it stays - // visible in dark mode, instead of painting the raw (black) SVG as a background. const styles = window.getComputedStyle(icon); - expect(styles.maskImage).toContain('https://example.com/x-icon.svg'); - expect(styles.backgroundImage).not.toContain('https://example.com/x-icon.svg'); - }); - - 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(); + expect(styles.maskImage).toContain(iconUrl); + expect(styles.backgroundImage).not.toContain(iconUrl); }); it('applies background-image styles for non-mask-image providers', async () => { @@ -147,6 +82,10 @@ describe('ProviderIcon', () => { const icon = screen.getByLabelText('Google icon'); expect(icon).toBeInTheDocument(); + + const styles = window.getComputedStyle(icon); + expect(styles.backgroundImage).toContain('https://example.com/google-icon.svg'); + expect(styles.maskImage).not.toContain('https://example.com/google-icon.svg'); }); }); diff --git a/packages/ui/src/hooks/__tests__/useEnabledThirdPartyProviders.test.tsx b/packages/ui/src/hooks/__tests__/useEnabledThirdPartyProviders.test.tsx index 84d94bd6e12..6824b96bafb 100644 --- a/packages/ui/src/hooks/__tests__/useEnabledThirdPartyProviders.test.tsx +++ b/packages/ui/src/hooks/__tests__/useEnabledThirdPartyProviders.test.tsx @@ -30,6 +30,7 @@ describe('useEnabledThirdPartyProviders', () => { const { authenticatableOauthStrategies } = result.current; expect(authenticatableOauthStrategies).toStrictEqual([ + 'oauth_agentid', 'oauth_apple', 'oauth_atlassian', 'oauth_bitbucket',