Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/oauth-consent-scope-checkboxes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': minor
---

Render each requested scope in `<OAuthConsent />` as a checkbox labelled by its description, checked by default. The checkbox can be targeted via the new `listGroupItemCheckbox` appearance element.
5 changes: 5 additions & 0 deletions .changeset/oauth-consent-single-scope-scrollbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Fix a stray scrollbar on the `<OAuthConsent />` and `<OAuthDeviceVerification />` scope list when only one scope is shown.
18 changes: 13 additions & 5 deletions packages/clerk-js/sandbox/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() ?? {
Expand Down
54 changes: 47 additions & 7 deletions packages/ui/src/components/OAuthConsent/ListGroup.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -179,3 +184,38 @@ export function ListGroupItemLabel(props: Omit<ComponentProps<typeof Text>, 'ele
/>
);
}

export function ListGroupCheckboxItem({
children,
defaultChecked,
}: {
children: ComponentProps<typeof Text>['children'];
defaultChecked?: boolean;
}) {
return (
<Box
as='li'
sx={listGroupItemStyles}
elementDescriptor={descriptors.listGroupItem}
>
<Flex
as='label'
align='start'
gap={2}
sx={{ cursor: 'pointer', userSelect: 'none' }}
>
<CheckboxInput
defaultChecked={defaultChecked}
focusRing={false}
elementDescriptor={descriptors.listGroupItemCheckbox}
sx={t => ({
flexShrink: 0,
cursor: 'pointer',
marginBlockStart: `calc((${t.fontSizes.$md} * ${t.lineHeights.$small} - ${t.sizes.$4}) / 2)`,
})}
/>
<ListGroupItemLabel as='span'>{children}</ListGroupItemLabel>
</Flex>
</Box>
);
}
18 changes: 7 additions & 11 deletions packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -274,9 +267,12 @@ function _OAuthConsent() {
</ListGroupHeader>
<ListGroupContent>
{displayedScopes.map(item => (
<ListGroupItem key={item.scope}>
<ListGroupItemLabel>{item.description || item.scope || ''}</ListGroupItemLabel>
</ListGroupItem>
<ListGroupCheckboxItem
key={item.scope}
defaultChecked
>
{item.description || item.scope || ''}
</ListGroupCheckboxItem>
))}
</ListGroupContent>
</ListGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<OAuthConsent />, { 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'] });
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/customizables/elementDescriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'listGroupContent',
'listGroupItem',
'listGroupItemLabel',
'listGroupItemCheckbox',

'header',
'headerTitle',
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/internal/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export type ElementsConfig = {
listGroupContent: WithOptions;
listGroupItem: WithOptions;
listGroupItemLabel: WithOptions;
listGroupItemCheckbox: WithOptions;

header: WithOptions;
headerTitle: WithOptions;
Expand Down
Loading