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;