Skip to content

feat(ui): Brand known OAuth clients on the consent screen#9158

Open
alexcarpenter wants to merge 2 commits into
mainfrom
oauth-consent-claude-logo
Open

feat(ui): Brand known OAuth clients on the consent screen#9158
alexcarpenter wants to merge 2 commits into
mainfrom
oauth-consent-claude-logo

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jul 14, 2026

Copy link
Copy Markdown
Member

Description

When an OAuth application has not uploaded its own logo, the consent screen now renders a recognizable brand mark for known clients (Claude, ChatGPT) in place of the default lock icon. Recognition is keyed on the request's trusted, backend-resolved registrable redirect domain (claude.ai, chatgpt.com, etc.), never the app-owner-set name or homepage, so a look-alike application name cannot borrow the branding. A new knownClients registry maps domains to brand icons and LogoGroupIcon was extended to accept an optional icon/tint (defaulting to the existing lock); an app's own uploaded logo always takes precedence.

Previews:

Screenshot 2026-07-14 at 9 31 00 AM Screenshot 2026-07-14 at 9 31 58 AM
Screenshot 2026-07-14 at 9 31 23 AM Screenshot 2026-07-14 at 9 31 41 AM

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • New Features

    • OAuth consent screens now show recognizable brand marks for supported clients, including Claude and ChatGPT, when no application logo is provided.
    • Branding is detected from the OAuth redirect domain, with support for common domain variations and case differences.
  • Bug Fixes

    • Improved fallback branding when OAuth applications do not provide a logo.

When an OAuth app has no uploaded logo, the consent screen now renders a
recognizable brand mark for known clients (Claude, ChatGPT). Matching is
keyed on the trusted, backend-resolved redirect domain, not the
app-owner-set name, so a look-alike name cannot borrow the branding.
@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a3c090a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@clerk/ui Patch
@clerk/chrome-extension Patch
@clerk/swingset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 14, 2026 2:50pm
swingset Ready Ready Preview, Comment Jul 14, 2026 2:50pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 1779648f-fc21-4f32-8deb-24d81cded460

📥 Commits

Reviewing files that changed from the base of the PR and between f126dd2 and a3c090a.

⛔ Files ignored due to path filters (2)
  • packages/ui/src/components/OAuthConsent/claude.svg is excluded by !**/*.svg
  • packages/ui/src/components/OAuthConsent/openai.svg is excluded by !**/*.svg
📒 Files selected for processing (3)
  • packages/ui/bundlewatch.config.json
  • packages/ui/src/components/OAuthConsent/brandIcons.ts
  • packages/ui/src/components/OAuthConsent/knownClients.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui/src/components/OAuthConsent/knownClients.ts

📝 Walkthrough

Walkthrough

OAuth consent now recognizes Claude and ChatGPT from registrable redirect domains and displays their brand icons when no application logo is provided. Logo fallback icons accept configurable components and styling, with tests, bundle-size configuration, and a patch changeset added.

Changes

OAuth consent branding

Layer / File(s) Summary
Known client registry and resolution
packages/ui/src/components/OAuthConsent/brandIcons.ts, packages/ui/src/components/OAuthConsent/knownClients.ts, packages/ui/src/components/OAuthConsent/__tests__/knownClients.test.ts
Defines Claude and ChatGPT brand icons, registrable-domain mappings, normalization, and resolver tests.
Configurable logo group icons
packages/ui/src/components/OAuthConsent/LogoGroup.tsx
Allows LogoGroupIcon to receive custom icon components and styling while preserving existing defaults.
Consent fallback branding
packages/ui/src/components/OAuthConsent/OAuthConsent.tsx, packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx, packages/ui/bundlewatch.config.json, .changeset/oauth-consent-known-clients.md
Uses known-client metadata in missing-logo branches, tests branded rendering, raises the OAuth consent bundle threshold, and adds a patch changeset.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OAuthConsent
  participant getKnownOAuthClient
  participant LogoGroupIcon
  OAuthConsent->>getKnownOAuthClient: Resolve redirect domain without an uploaded logo
  getKnownOAuthClient-->>OAuthConsent: Return recognized client icon and styling
  OAuthConsent->>LogoGroupIcon: Render the fallback brand icon
Loading

Suggested reviewers: jacekradko

Poem

A rabbit hops through OAuth’s gate,
Claude and ChatGPT badges wait.
Domains guide the icons bright,
Fallback marks now look just right.
Hop, hop—consent takes flight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: branding known OAuth clients on the consent screen.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9158

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9158

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9158

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9158

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9158

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9158

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9158

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9158

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9158

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9158

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9158

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9158

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9158

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9158

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9158

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9158

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9158

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9158

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9158

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9158

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9158

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9158

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9158

commit: a3c090a

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-14T14:51:45.213Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on a3c090a.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx`:
- Around line 546-565: The OAuthConsent branded-logo test is not verifying
client recognition because it asserts only the generic .cl-logoGroupIcon and
uses the branded application name. Update the test around createFixtures and
mockOAuthApplication to use a non-brand application name while retaining
Claude’s recognized domain, then assert Claude’s distinctive rendered icon. Add
a separate unknown-domain case that verifies the lock icon fallback.
- Line 551: Remove the as any cast from the setProps call in the OAuthConsent
test and pass a value typed as OAuthConsentProps. Reuse the component name
already supplied by bindCreateFixtures('OAuthConsent') rather than duplicating
or bypassing the fixture context typing.

In `@packages/ui/src/components/OAuthConsent/LogoGroup.tsx`:
- Around line 38-48: Add an explicit JSX return type to the exported
LogoGroupIcon function while preserving its existing props, defaults, and
rendering behavior. Use the project’s established React component return-type
convention.
- Around line 41-42: Update the default iconSx in LogoGroupIcon so the
$primary500 color fallback is applied only when icon === LockDottedCircle; leave
iconSx undefined for custom icons, preserving their native styling unless
callers explicitly provide styling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: ff882885-aa09-4710-84d2-82573abc5d84

📥 Commits

Reviewing files that changed from the base of the PR and between 56b8683 and f126dd2.

⛔ Files ignored due to path filters (2)
  • packages/ui/src/icons/claude.svg is excluded by !**/*.svg
  • packages/ui/src/icons/openai.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • .changeset/oauth-consent-known-clients.md
  • packages/ui/src/components/OAuthConsent/LogoGroup.tsx
  • packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
  • packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx
  • packages/ui/src/components/OAuthConsent/__tests__/knownClients.test.ts
  • packages/ui/src/components/OAuthConsent/knownClients.ts
  • packages/ui/src/icons/index.ts

Comment on lines +546 to +565
it('renders the branded logo badge for a recognized client with no uploaded logo', async () => {
const { wrapper, fixtures, props } = await createFixtures(f => {
f.withUser({ email_addresses: ['jane@example.com'] });
});

props.setProps({ componentName: 'OAuthConsent' } as any);
mockOAuthApplication(fixtures.clerk, {
getConsentInfo: vi.fn().mockResolvedValue({
...fakeConsentInfo,
oauthApplicationName: 'Claude',
oauthApplicationLogoUrl: '',
redirectDomain: 'claude.ai',
}),
});

const { getByText, baseElement } = render(<OAuthConsent />, { wrapper });

await waitFor(() => {
expect(getByText('Claude')).toBeVisible();
expect(baseElement.querySelector('.cl-logoGroupIcon')).not.toBeNull();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the branded icon, not only the generic badge.

This test can pass when getKnownOAuthClient returns undefined: every no-logo application still renders .cl-logoGroupIcon, and the “Claude” title comes directly from oauthApplicationName. Use a non-brand application name and assert Claude’s distinctive rendered icon; also add an unknown-domain case to verify the lock fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx`
around lines 546 - 565, The OAuthConsent branded-logo test is not verifying
client recognition because it asserts only the generic .cl-logoGroupIcon and
uses the branded application name. Update the test around createFixtures and
mockOAuthApplication to use a non-brand application name while retaining
Claude’s recognized domain, then assert Claude’s distinctive rendered icon. Add
a separate unknown-domain case that verifies the lock icon fallback.

f.withUser({ email_addresses: ['jane@example.com'] });
});

props.setProps({ componentName: 'OAuthConsent' } as any);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '500,580p' packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx

Repository: clerk/javascript

Length of output: 2681


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '500,580p' packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx

Repository: clerk/javascript

Length of output: 2681


🏁 Script executed:

rg -n "createFixtures|setProps\\(|componentName" packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx packages/ui/src -g '*.{ts,tsx}'

Repository: clerk/javascript

Length of output: 50373


🏁 Script executed:

sed -n '1,220p' packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx

printf '\n---\n'

rg -n "function createFixtures|const createFixtures|type .*props|setProps" packages/ui/src/components/OAuthConsent/__tests__ -g '*.{ts,tsx}'

printf '\n---\n'

rg -n "componentName" packages/ui/src/components/OAuthConsent packages/ui/src -g '*.{ts,tsx}'

Repository: clerk/javascript

Length of output: 33642


🏁 Script executed:

rg -n "createFixtures|setProps|componentName" packages/ui/src -g '*.{ts,tsx}'

Repository: clerk/javascript

Length of output: 50373


🏁 Script executed:

git ls-files 'packages/ui/src/**/*test*' 'packages/ui/src/**/__tests__/*' | sed -n '1,200p'

Repository: clerk/javascript

Length of output: 14571


🏁 Script executed:

sed -n '1,220p' packages/ui/src/test/create-fixtures.tsx

printf '\n---\n'

sed -n '1,260p' packages/ui/src/types.ts

Repository: clerk/javascript

Length of output: 12052


🏁 Script executed:

sed -n '1,220p' packages/ui/src/test/create-fixtures.tsx

printf '\n---\n'

sed -n '150,220p' packages/ui/src/types.ts

Repository: clerk/javascript

Length of output: 7486


🏁 Script executed:

cat -n packages/ui/src/test/create-fixtures.tsx | sed -n '1,220p'

Repository: clerk/javascript

Length of output: 6436


🏁 Script executed:

sed -n '1,260p' packages/ui/src/contexts/ClerkUIComponentsContext.tsx

printf '\n---\n'

rg -n "type AvailableComponentProps|interface AvailableComponentProps|ComponentContextProvider" packages/ui/src -g '*.{ts,tsx}'

Repository: clerk/javascript

Length of output: 7493


🏁 Script executed:

rg -n "export (const|function) ComponentContextProvider|type ComponentContextProvider|interface ComponentContextProvider" packages/ui/src/contexts/ClerkUIComponentsContext.tsx packages/ui/src/contexts -g '*.{ts,tsx}'

printf '\n---\n'

sed -n '1,220p' packages/ui/src/contexts/ClerkUIComponentsContext.tsx

Repository: clerk/javascript

Length of output: 6629


🏁 Script executed:

rg -n "type OAuthConsentProps|interface OAuthConsentProps" . -g '*.{ts,tsx,d.ts}'

Repository: clerk/javascript

Length of output: 230


🏁 Script executed:

rg -n "OAuthConsentProps" packages node_modules -g '*.{ts,tsx,d.ts}' --max-count 20

Repository: clerk/javascript

Length of output: 2583


🏁 Script executed:

rg -n "OAuthConsentProps" packages -g '*.{ts,tsx,d.ts}' --max-count 50

Repository: clerk/javascript

Length of output: 2530


Drop the as any cast here. bindCreateFixtures('OAuthConsent') already supplies the component name; pass a typed OAuthConsentProps value for the fixture context instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx` at
line 551, Remove the as any cast from the setProps call in the OAuthConsent test
and pass a value typed as OAuthConsentProps. Reuse the component name already
supplied by bindCreateFixtures('OAuthConsent') rather than duplicating or
bypassing the fixture context typing.

Source: Coding guidelines

Comment on lines +38 to +48
export function LogoGroupIcon({
size = 'md',
sx,
icon = LockDottedCircle,
iconSx = t => ({ color: t.colors.$primary500 }),
}: {
size?: 'sm' | 'md';
sx?: ThemableCssProp;
icon?: React.ComponentType;
iconSx?: ThemableCssProp;
}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
git ls-files 'packages/ui/src/components/OAuthConsent/LogoGroup.tsx' && \
ast-grep outline packages/ui/src/components/OAuthConsent/LogoGroup.tsx --view expanded && \
cat -n packages/ui/src/components/OAuthConsent/LogoGroup.tsx | sed -n '1,220p'

Repository: clerk/javascript

Length of output: 3691


Add an explicit return type to LogoGroupIcon. It’s an exported TypeScript component, so annotating the return type keeps the API contract explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/OAuthConsent/LogoGroup.tsx` around lines 38 - 48,
Add an explicit JSX return type to the exported LogoGroupIcon function while
preserving its existing props, defaults, and rendering behavior. Use the
project’s established React component return-type convention.

Sources: Coding guidelines, Learnings

Comment on lines +41 to +42
icon = LockDottedCircle,
iconSx = t => ({ color: t.colors.$primary500 }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files 'packages/ui/src/components/OAuthConsent/LogoGroup.tsx' 'packages/ui/src/**/knownClients.ts' 'packages/ui/src/**/knownClients.*' 'packages/ui/src/**/LogoGroup.tsx' && \
printf '\n--- LogoGroup.tsx ---\n' && cat -n packages/ui/src/components/OAuthConsent/LogoGroup.tsx && \
printf '\n--- knownClients search ---\n' && rg -n "iconSx|LogoGroup" packages/ui/src -g '!**/node_modules/**'

Repository: clerk/javascript

Length of output: 9087


🏁 Script executed:

git ls-files 'packages/ui/src/components/OAuthConsent/LogoGroup.tsx' 'packages/ui/src/**/knownClients.ts' 'packages/ui/src/**/knownClients.*' 'packages/ui/src/**/LogoGroup.tsx' && printf '\n--- LogoGroup.tsx ---\n' && cat -n packages/ui/src/components/OAuthConsent/LogoGroup.tsx && printf '\n--- knownClients search ---\n' && rg -n "iconSx|LogoGroup" packages/ui/src -g '!**/node_modules/**'

Repository: clerk/javascript

Length of output: 9087


🏁 Script executed:

sed -n '1,120p' packages/ui/src/components/OAuthConsent/knownClients.ts && printf '\n--- OAuthConsent.tsx excerpt ---\n' && sed -n '150,240p' packages/ui/src/components/OAuthConsent/OAuthConsent.tsx && printf '\n--- knownClients test excerpt ---\n' && sed -n '1,220p' packages/ui/src/components/OAuthConsent/__tests__/knownClients.test.ts

Repository: clerk/javascript

Length of output: 6619


Don’t tint custom icons by default.

LogoGroupIcon always falls back to $primary500 when iconSx is omitted, so known-client icons like Claude inherit the lock tint unless they override it explicitly. Gate the default on icon === LockDottedCircle so only the built-in icon gets the fallback color.

Proposed fix
-  iconSx = t => ({ color: t.colors.$primary500 }),
+  iconSx = icon === LockDottedCircle ? t => ({ color: t.colors.$primary500 }) : undefined,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
icon = LockDottedCircle,
iconSx = t => ({ color: t.colors.$primary500 }),
icon = LockDottedCircle,
iconSx = icon === LockDottedCircle ? t => ({ color: t.colors.$primary500 }) : undefined,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/OAuthConsent/LogoGroup.tsx` around lines 41 - 42,
Update the default iconSx in LogoGroupIcon so the $primary500 color fallback is
applied only when icon === LockDottedCircle; leave iconSx undefined for custom
icons, preserving their native styling unless callers explicitly provide
styling.

The Claude/ChatGPT brand SVGs lived in src/icons, which the ui-common
rspack cacheGroup captures, pushing the shared chunk over its bundlewatch
budget. Move them into the OAuthConsent component directory so they bundle
into the lazy oauthConsent chunk instead, and bump that chunk's budget to
account for the marks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant