diff --git a/.changeset/oauth-consent-scope-checkboxes.md b/.changeset/oauth-consent-scope-checkboxes.md
new file mode 100644
index 00000000000..b497a494afe
--- /dev/null
+++ b/.changeset/oauth-consent-scope-checkboxes.md
@@ -0,0 +1,5 @@
+---
+'@clerk/ui': minor
+---
+
+Render each requested scope in `` as a checkbox labelled by its description, checked by default. The checkbox can be targeted via the new `listGroupItemCheckbox` appearance element.
diff --git a/.changeset/oauth-consent-single-scope-scrollbar.md b/.changeset/oauth-consent-single-scope-scrollbar.md
new file mode 100644
index 00000000000..ffa84d92306
--- /dev/null
+++ b/.changeset/oauth-consent-single-scope-scrollbar.md
@@ -0,0 +1,5 @@
+---
+'@clerk/ui': patch
+---
+
+Fix a stray scrollbar on the `` and `` scope list when only one scope is shown.
diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts
index bf9d3c8b9bf..eb79772a128 100644
--- a/packages/clerk-js/sandbox/app.ts
+++ b/packages/clerk-js/sandbox/app.ts
@@ -466,11 +466,19 @@ void (async () => {
},
'/oauth-consent': () => {
const searchParams = new URLSearchParams(window.location.search);
- const scopes = (searchParams.get('scope')?.split(',') ?? []).map(scope => ({
- scope,
- description: scope === 'offline_access' ? null : `Grants access to your ${scope}`,
- requires_consent: true,
- }));
+ const scopes = [
+ ...(searchParams.get('scope')?.split(',') ?? []).map(scope => ({
+ scope,
+ description: scope === 'offline_access' ? null : `Grants access to your ${scope}`,
+ requires_consent: true,
+ })),
+ {
+ scope: 'sandbox:long_description',
+ description:
+ 'Grants access to read and update your profile, connected accounts, and organization memberships across every workspace you belong to',
+ requires_consent: true,
+ },
+ ];
Clerk.mountOAuthConsent(
app,
componentControls.oauthConsent.getProps() ?? {
diff --git a/packages/ui/src/components/OAuthConsent/ListGroup.tsx b/packages/ui/src/components/OAuthConsent/ListGroup.tsx
index 9894228fb7d..fc956b3d267 100644
--- a/packages/ui/src/components/OAuthConsent/ListGroup.tsx
+++ b/packages/ui/src/components/OAuthConsent/ListGroup.tsx
@@ -1,6 +1,7 @@
import type { ComponentProps } from 'react';
-import { Box, descriptors, Text } from '@/ui/customizables';
+import { Box, CheckboxInput, descriptors, Flex, Text } from '@/ui/customizables';
+import type { InternalTheme } from '@/ui/styledSystem';
import { common } from '@/ui/styledSystem';
import { colors } from '@/ui/utils/colors';
@@ -74,7 +75,7 @@ export function ListGroupContent({
backgroundColor: t.colors.$colorBackground,
overflowY: 'auto',
...common.unstyledScrollbar(t),
- '--fade-distance': '2.5rem',
+ '--fade-distance': '2rem',
'&::before, &::after': {
content: '""',
display: 'block',
@@ -131,6 +132,14 @@ export function ListGroupContent({
);
}
+const listGroupItemStyles = (t: InternalTheme) => ({
+ paddingInline: t.space.$3,
+ paddingBlock: t.space.$2,
+ borderTopWidth: t.borderWidths.$normal,
+ borderTopStyle: t.borderStyles.$solid,
+ borderTopColor: t.colors.$borderAlpha100,
+});
+
export function ListGroupItem({
children,
sx,
@@ -142,13 +151,9 @@ export function ListGroupItem({
as='li'
sx={[
t => ({
+ ...listGroupItemStyles(t),
display: 'flex',
alignItems: 'baseline',
- paddingInline: t.space.$3,
- paddingBlock: t.space.$2,
- borderTopWidth: t.borderWidths.$normal,
- borderTopStyle: t.borderStyles.$solid,
- borderTopColor: t.colors.$borderAlpha100,
'&::before': {
content: '""',
display: 'inline-block',
@@ -179,3 +184,38 @@ export function ListGroupItemLabel(props: Omit, 'ele
/>
);
}
+
+export function ListGroupCheckboxItem({
+ children,
+ defaultChecked,
+}: {
+ children: ComponentProps['children'];
+ defaultChecked?: boolean;
+}) {
+ return (
+
+
+ ({
+ flexShrink: 0,
+ cursor: 'pointer',
+ marginBlockStart: `calc((${t.fontSizes.$md} * ${t.lineHeights.$small} - ${t.sizes.$4}) / 2)`,
+ })}
+ />
+ {children}
+
+
+ );
+}
diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
index b9f6fcd00cb..05c32b7340a 100644
--- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
+++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
@@ -14,14 +14,7 @@ import { Route, Switch } from '@/ui/router';
import { InlineAction } from './InlineAction';
import { getKnownOAuthClient } from './knownClients';
-import {
- ListGroup,
- ListGroupContent,
- ListGroupHeader,
- ListGroupHeaderTitle,
- ListGroupItem,
- ListGroupItemLabel,
-} from './ListGroup';
+import { ListGroup, ListGroupCheckboxItem, ListGroupContent, ListGroupHeader, ListGroupHeaderTitle } from './ListGroup';
import { LogoGroup, LogoGroupIcon, LogoGroupItem, LogoGroupItemContainer, LogoGroupSeparator } from './LogoGroup';
import { OrgSelect } from './OrgSelect';
import { getForwardedParams, getOAuthConsentFromSearch, getRedirectDisplay, getRedirectUriFromSearch } from './utils';
@@ -274,9 +267,12 @@ function _OAuthConsent() {
{displayedScopes.map(item => (
-
- {item.description || item.scope || ''}
-
+
+ {item.description || item.scope || ''}
+
))}
diff --git a/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx b/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx
index 34377f75200..56efe614919 100644
--- a/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx
+++ b/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx
@@ -109,6 +109,29 @@ describe('OAuthConsent', () => {
});
});
+ it('renders each scope as a checkbox labelled by its description, checked by default', 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),
+ });
+
+ const { findByRole, getByText, userEvent } = render(, { wrapper });
+
+ const identity = await findByRole('checkbox', { name: 'View your identity' });
+ const email = await findByRole('checkbox', { name: 'Access your email address' });
+ expect(identity).toBeChecked();
+ expect(email).toBeChecked();
+
+ await userEvent.click(getByText('Access your email address'));
+
+ expect(email).not.toBeChecked();
+ expect(identity).toBeChecked();
+ });
+
it('identifies private metadata as potentially sensitive data set by the Clerk application', async () => {
const { wrapper, fixtures, props } = await createFixtures(f => {
f.withUser({ email_addresses: ['jane@example.com'] });
diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts
index e1ff3112d55..f1d880951bc 100644
--- a/packages/ui/src/customizables/elementDescriptors.ts
+++ b/packages/ui/src/customizables/elementDescriptors.ts
@@ -65,6 +65,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'listGroupContent',
'listGroupItem',
'listGroupItemLabel',
+ 'listGroupItemCheckbox',
'header',
'headerTitle',
diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts
index 09fc4ffad1a..85da40d1706 100644
--- a/packages/ui/src/internal/appearance.ts
+++ b/packages/ui/src/internal/appearance.ts
@@ -193,6 +193,7 @@ export type ElementsConfig = {
listGroupContent: WithOptions;
listGroupItem: WithOptions;
listGroupItemLabel: WithOptions;
+ listGroupItemCheckbox: WithOptions;
header: WithOptions;
headerTitle: WithOptions;