From 11ddc84b93f554b370fee12ebc6edb26537b18a4 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 10 Sep 2026 16:05:02 -0600 Subject: [PATCH 1/4] feat(shared,ui): add AgentID OAuth provider --- .changeset/agentid-oauth-provider.md | 6 ++++++ packages/shared/src/oauth.ts | 6 ++++++ packages/shared/src/types/oauth.ts | 3 +++ packages/ui/src/common/ProviderIcon.tsx | 2 +- .../common/__tests__/ProviderIcon.test.tsx | 20 +++++++++++++++++++ .../useEnabledThirdPartyProviders.test.tsx | 1 + 6 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/agentid-oauth-provider.md diff --git a/.changeset/agentid-oauth-provider.md b/.changeset/agentid-oauth-provider.md new file mode 100644 index 00000000000..6333f6da924 --- /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. `` now renders the AgentID mark with the foreground color so it stays visible in dark mode. diff --git a/packages/shared/src/oauth.ts b/packages/shared/src/oauth.ts index 68360d5ae46..cfb71123018 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://www.agentid.com/docs/clerk', + }, ]; 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/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx index 0a2a6d4b425..50019ce5688 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; +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..b9aa58c69e2 100644 --- a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx +++ b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx @@ -133,6 +133,26 @@ describe('ProviderIcon', () => { expect(icon).toBeInTheDocument(); }); + it('applies mask-image styles for supported providers (agentid)', async () => { + const { wrapper } = await createFixtures(); + + render( + , + { wrapper }, + ); + + const icon = screen.getByLabelText('AgentID icon'); + expect(icon).toBeInTheDocument(); + + const styles = window.getComputedStyle(icon); + expect(styles.maskImage).toContain('https://example.com/agentid-icon.svg'); + expect(styles.backgroundImage).not.toContain('https://example.com/agentid-icon.svg'); + }); + it('applies background-image styles for non-mask-image providers', async () => { const { wrapper } = await createFixtures(); 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', From 3ecf487c20bc9f2c3a3775618c47ea6c1473c180 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 11 Sep 2026 14:05:11 -0600 Subject: [PATCH 2/4] test(ui): cover the provider mask-image list exhaustively The ProviderIcon tests hand-listed each provider in SUPPORTS_MASK_IMAGE, and four of the six cases only asserted the icon rendered, not that mask-image was applied. A provider could be added to (or dropped from) the list without any test noticing, and the dark-mode tinting for Apple, GitHub, OKX Wallet and Vercel was effectively untested. Review of the AgentID addition flagged this. Export SUPPORTS_MASK_IMAGE and drive the test with it.each over the constant, asserting for every id that mask-image carries the icon URL and background-image does not, plus one negative case (google) asserting the inverse. Any future edit to the list is now covered automatically. The changeset also overstated the AgentID dark-mode claim as blanket visibility; it now scopes the claim to (the social button icon), since the Mosaic profile provider icon renders a plain and is unaffected. --- .changeset/agentid-oauth-provider.md | 2 +- packages/ui/src/common/ProviderIcon.tsx | 2 +- .../common/__tests__/ProviderIcon.test.tsx | 113 +++--------------- 3 files changed, 17 insertions(+), 100 deletions(-) diff --git a/.changeset/agentid-oauth-provider.md b/.changeset/agentid-oauth-provider.md index 6333f6da924..672dadddeae 100644 --- a/.changeset/agentid-oauth-provider.md +++ b/.changeset/agentid-oauth-provider.md @@ -3,4 +3,4 @@ '@clerk/ui': patch --- -Add `agentid` to `OAuthProvider` and `OAUTH_PROVIDERS` to support the "Continue with AgentID" OAuth flow. `` now renders the AgentID mark with the foreground color so it stays visible in dark mode. +Add `agentid` to `OAuthProvider` and `OAUTH_PROVIDERS` to support the "Continue with AgentID" OAuth flow. `` renders the AgentID mark via mask-image so the social button icon follows the foreground color. diff --git a/packages/ui/src/common/ProviderIcon.tsx b/packages/ui/src/common/ProviderIcon.tsx index 50019ce5688..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 = ['agentid', '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 b9aa58c69e2..0527aecf045 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,114 +43,27 @@ describe('ProviderIcon', () => { expect(icon).toBeInTheDocument(); }); - it('applies mask-image styles for supported providers (apple)', async () => { + // 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. + it.each(SUPPORTS_MASK_IMAGE)('applies mask-image styles for supported provider %s', async id => { const { wrapper } = await createFixtures(); + const iconUrl = `https://example.com/${id}-icon.svg`; 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('applies mask-image styles for supported providers (okx_wallet)', async () => { - const { wrapper } = await createFixtures(); - - 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(); - }); - - it('applies mask-image styles for supported providers (agentid)', async () => { - const { wrapper } = await createFixtures(); - - render( - , - { wrapper }, - ); - - const icon = screen.getByLabelText('AgentID icon'); - expect(icon).toBeInTheDocument(); - - const styles = window.getComputedStyle(icon); - expect(styles.maskImage).toContain('https://example.com/agentid-icon.svg'); - expect(styles.backgroundImage).not.toContain('https://example.com/agentid-icon.svg'); + expect(styles.maskImage).toContain(iconUrl); + expect(styles.backgroundImage).not.toContain(iconUrl); }); it('applies background-image styles for non-mask-image providers', async () => { @@ -167,6 +80,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'); }); }); From c8f6c4c681587716af7bea1bbe4512c4519bcf8f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 11 Sep 2026 14:14:50 -0600 Subject: [PATCH 3/4] test(ui): pin the mask-image provider list The it.each over SUPPORTS_MASK_IMAGE is self-referential: removing agentid (or any provider) from the list just runs one fewer case and stays green, so the only UI behaviour change in the AgentID PR had no regression guard. Add a full-list equality assertion beside the parameterized block so any membership or order change to SUPPORTS_MASK_IMAGE fails a test until the expectation is updated deliberately. The changeset also dropped the "social button" qualifier: the mask branch keys purely off the provider id, and ProviderIcon renders in Connected Accounts, Enterprise Accounts, Web3 and phone-code surfaces too. --- .changeset/agentid-oauth-provider.md | 2 +- packages/ui/src/common/__tests__/ProviderIcon.test.tsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.changeset/agentid-oauth-provider.md b/.changeset/agentid-oauth-provider.md index 672dadddeae..b3362687914 100644 --- a/.changeset/agentid-oauth-provider.md +++ b/.changeset/agentid-oauth-provider.md @@ -3,4 +3,4 @@ '@clerk/ui': patch --- -Add `agentid` to `OAuthProvider` and `OAUTH_PROVIDERS` to support the "Continue with AgentID" OAuth flow. `` renders the AgentID mark via mask-image so the social button icon follows the foreground color. +Add `agentid` to `OAuthProvider` and `OAUTH_PROVIDERS` to support the "Continue with AgentID" OAuth flow. `` renders the AgentID mark via mask-image so the icon follows the foreground color. diff --git a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx index 0527aecf045..7c4cd2697bb 100644 --- a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx +++ b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx @@ -43,6 +43,10 @@ describe('ProviderIcon', () => { expect(icon).toBeInTheDocument(); }); + it('keeps the mask-image provider list in sync', () => { + expect(SUPPORTS_MASK_IMAGE).toEqual(['agentid', 'apple', 'github', 'okx_wallet', 'vercel', 'x']); + }); + // 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. it.each(SUPPORTS_MASK_IMAGE)('applies mask-image styles for supported provider %s', async id => { From 4887afe1404563c38cf044c1d7230876d8b2bd36 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 11 Sep 2026 16:42:45 -0600 Subject: [PATCH 4/4] test(ui): move the mask-image rationale into the test name Drops the multi-line comment above the mask-image it.each block per the repo's no-code-comments rule; the tinting rationale now reads from the test name instead. --- packages/ui/src/common/__tests__/ProviderIcon.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx index 7c4cd2697bb..5d1281bd5f2 100644 --- a/packages/ui/src/common/__tests__/ProviderIcon.test.tsx +++ b/packages/ui/src/common/__tests__/ProviderIcon.test.tsx @@ -47,9 +47,7 @@ describe('ProviderIcon', () => { expect(SUPPORTS_MASK_IMAGE).toEqual(['agentid', 'apple', 'github', 'okx_wallet', 'vercel', 'x']); }); - // 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. - it.each(SUPPORTS_MASK_IMAGE)('applies mask-image styles for supported provider %s', async id => { + 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`;