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',