From 8a8e4f47b53da812ea8bdc230add4fe71516d134 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 8 Apr 2026 15:18:39 -0400 Subject: [PATCH 1/6] init --- .../components/OAuthConsent/OAuthConsent.tsx | 101 +++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index 496eddb787a..aaf7c087381 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -3,14 +3,15 @@ import type { ComponentProps } from 'react'; import { useState } from 'react'; import { useEnvironment, useOAuthConsentContext } from '@/ui/contexts'; -import { Box, Button, Flex, Flow, Grid, Icon, Text } from '@/ui/customizables'; +import { Box, Button, Flex, Flow, Grid, Icon, Image, Span, Text } from '@/ui/customizables'; import { ApplicationLogo } from '@/ui/elements/ApplicationLogo'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { Modal } from '@/ui/elements/Modal'; +import { Select, SelectButton, SelectOptionList } from '@/ui/elements/Select'; import { Tooltip } from '@/ui/elements/Tooltip'; -import { LockDottedCircle } from '@/ui/icons'; +import { Check, LockDottedCircle } from '@/ui/icons'; import { Alert, Textarea } from '@/ui/primitives'; import type { ThemableCssProp } from '@/ui/styledSystem'; import { common } from '@/ui/styledSystem'; @@ -24,6 +25,18 @@ export function OAuthConsentInternal() { const { user } = useUser(); const { applicationName, logoImageUrl } = useEnvironment().displayConfig; const [isUriModalOpen, setIsUriModalOpen] = useState(false); + const [selectedValue, setSelectedValue] = useState('clerk-nation'); + + const selectOptions = [ + { value: 'clerk-nation', label: 'Clerk Nation', logoUrl: 'https://img.clerk.com/static/clerk.png' }, + { + value: 'perky-clerky', + label: 'Perky Clerky Clerk Nation Clerk Nation Clerk Nation', + logoUrl: 'https://img.clerk.com/static/clerk.png', + }, + { value: 'clerk', label: 'Clerk', logoUrl: 'https://img.clerk.com/static/clerk.png' }, + { value: 'clerk-of-oz', label: 'The Clerk of Oz', logoUrl: 'https://img.clerk.com/static/clerk.png' }, + ]; const primaryIdentifier = user?.primaryEmailAddress?.emailAddress || user?.primaryPhoneNumber?.phoneNumber; @@ -108,6 +121,90 @@ export function OAuthConsentInternal() { + + + + + ({ textAlign: 'start', From 34cdfc7973883c51f7a4730beda9676df2a5b6d9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 8 Apr 2026 15:37:41 -0400 Subject: [PATCH 2/6] fix alignment --- packages/ui/src/components/OAuthConsent/OAuthConsent.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index aaf7c087381..4ce639fef13 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -1,6 +1,6 @@ import { useUser } from '@clerk/shared/react'; import type { ComponentProps } from 'react'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useEnvironment, useOAuthConsentContext } from '@/ui/contexts'; import { Box, Button, Flex, Flow, Grid, Icon, Image, Span, Text } from '@/ui/customizables'; @@ -26,6 +26,7 @@ export function OAuthConsentInternal() { const { applicationName, logoImageUrl } = useEnvironment().displayConfig; const [isUriModalOpen, setIsUriModalOpen] = useState(false); const [selectedValue, setSelectedValue] = useState('clerk-nation'); + const selectButtonRef = useRef(null); const selectOptions = [ { value: 'clerk-nation', label: 'Clerk Nation', logoUrl: 'https://img.clerk.com/static/clerk.png' }, @@ -127,6 +128,7 @@ export function OAuthConsentInternal() { options={selectOptions} value={selectedValue} onChange={option => setSelectedValue(option.value)} + referenceElement={selectButtonRef} renderOption={(option, _index, isSelected) => ( ({ inlineSize: 'min(100%, 16rem)', paddingInline: theme.space.$3, From 64322b22e82978a1cf14cb7e75813b301e36e02f Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 8 Apr 2026 16:06:32 -0400 Subject: [PATCH 3/6] update spacing --- packages/ui/src/components/OAuthConsent/OAuthConsent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index 4ce639fef13..4453714de87 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -137,7 +137,8 @@ export function OAuthConsentInternal() { display: 'grid', gridTemplateColumns: `${theme.sizes.$5} 1fr ${theme.sizes.$3}`, columnGap: theme.space.$2, - paddingInline: theme.space.$3, + paddingInlineStart: theme.space.$1, + paddingInlineEnd: theme.space.$1x5, paddingBlock: theme.space.$1, alignItems: 'center', borderRadius: theme.radii.$md, @@ -160,6 +161,7 @@ export function OAuthConsentInternal() { sx={{ flex: 1, textAlign: 'start', minWidth: 0, maxInlineSize: '200px' }} truncate as='span' + variant='subtitle' > {option.label} From 529cf7310d0db9be65bc9cd3dcaa7df64631103a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 8 Apr 2026 16:32:58 -0400 Subject: [PATCH 4/6] extract org select --- .../components/OAuthConsent/OAuthConsent.tsx | 105 +++-------------- .../src/components/OAuthConsent/OrgSelect.tsx | 110 ++++++++++++++++++ 2 files changed, 124 insertions(+), 91 deletions(-) create mode 100644 packages/ui/src/components/OAuthConsent/OrgSelect.tsx diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index 4453714de87..0fcf18e1e20 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -1,22 +1,23 @@ import { useUser } from '@clerk/shared/react'; import type { ComponentProps } from 'react'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { useEnvironment, useOAuthConsentContext } from '@/ui/contexts'; -import { Box, Button, Flex, Flow, Grid, Icon, Image, Span, Text } from '@/ui/customizables'; +import { Box, Button, Flex, Flow, Grid, Icon, Text } from '@/ui/customizables'; import { ApplicationLogo } from '@/ui/elements/ApplicationLogo'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { Modal } from '@/ui/elements/Modal'; -import { Select, SelectButton, SelectOptionList } from '@/ui/elements/Select'; import { Tooltip } from '@/ui/elements/Tooltip'; -import { Check, LockDottedCircle } from '@/ui/icons'; +import { LockDottedCircle } from '@/ui/icons'; import { Alert, Textarea } from '@/ui/primitives'; import type { ThemableCssProp } from '@/ui/styledSystem'; import { common } from '@/ui/styledSystem'; import { colors } from '@/ui/utils/colors'; +import { OrgSelect } from './OrgSelect'; + const OFFLINE_ACCESS_SCOPE = 'offline_access'; export function OAuthConsentInternal() { @@ -26,7 +27,6 @@ export function OAuthConsentInternal() { const { applicationName, logoImageUrl } = useEnvironment().displayConfig; const [isUriModalOpen, setIsUriModalOpen] = useState(false); const [selectedValue, setSelectedValue] = useState('clerk-nation'); - const selectButtonRef = useRef(null); const selectOptions = [ { value: 'clerk-nation', label: 'Clerk Nation', logoUrl: 'https://img.clerk.com/static/clerk.png' }, @@ -123,92 +123,15 @@ export function OAuthConsentInternal() { - - - + {selectOptions.length > 0 && ( + + + + )} ({ diff --git a/packages/ui/src/components/OAuthConsent/OrgSelect.tsx b/packages/ui/src/components/OAuthConsent/OrgSelect.tsx new file mode 100644 index 00000000000..10cba35f52f --- /dev/null +++ b/packages/ui/src/components/OAuthConsent/OrgSelect.tsx @@ -0,0 +1,110 @@ +import { useRef } from 'react'; + +import { Box, Icon, Image, Text } from '@/ui/customizables'; +import { Select, SelectButton, SelectOptionList } from '@/ui/elements/Select'; +import { Check } from '@/ui/icons'; +import { common } from '@/ui/styledSystem'; + +export type OrgOption = { + value: string; + label: string; + logoUrl: string; +}; + +type OrgSelectProps = { + options: OrgOption[]; + value: string | null; + onChange: (value: string) => void; +}; + +export function OrgSelect({ options, value, onChange }: OrgSelectProps) { + const buttonRef = useRef(null); + const selected = options.find(option => option.value === value); + + return ( + + ); +} From 86d9c2fc28bb89c4af6e68a3269d2da04ee83ff4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 10 Apr 2026 09:40:42 -0400 Subject: [PATCH 5/6] refactor(ui): Extract OAuth LogoGroup components (#8282) --- .../src/components/OAuthConsent/ListGroup.tsx | 123 ++++++++++ .../src/components/OAuthConsent/LogoGroup.tsx | 100 ++++++++ .../components/OAuthConsent/OAuthConsent.tsx | 228 ++++-------------- .../src/components/OAuthConsent/OrgSelect.tsx | 1 + .../src/customizables/elementDescriptors.ts | 12 + packages/ui/src/internal/appearance.ts | 12 + 6 files changed, 295 insertions(+), 181 deletions(-) create mode 100644 packages/ui/src/components/OAuthConsent/ListGroup.tsx create mode 100644 packages/ui/src/components/OAuthConsent/LogoGroup.tsx diff --git a/packages/ui/src/components/OAuthConsent/ListGroup.tsx b/packages/ui/src/components/OAuthConsent/ListGroup.tsx new file mode 100644 index 00000000000..8ee57fe1f9f --- /dev/null +++ b/packages/ui/src/components/OAuthConsent/ListGroup.tsx @@ -0,0 +1,123 @@ +import { Box, Text, descriptors } from '@/ui/customizables'; +import { common } from '@/ui/styledSystem'; +import { colors } from '@/ui/utils/colors'; +import type { ComponentProps } from 'react'; + +export function ListGroup({ children, sx, ...props }: Omit, 'elementDescriptor'>) { + return ( + ({ + textAlign: 'start', + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$borderAlpha100, + borderRadius: t.radii.$lg, + overflow: 'hidden', + }), + sx, + ]} + elementDescriptor={descriptors.listGroup} + > + {children} + + ); +} + +export function ListGroupHeader({ children, sx, ...props }: Omit, 'elementDescriptor'>) { + return ( + ({ + padding: t.space.$3, + background: common.mergedColorsBackground( + colors.setAlpha(t.colors.$colorBackground, 1), + t.colors.$neutralAlpha50, + ), + }), + sx, + ]} + elementDescriptor={descriptors.listGroupHeader} + > + {children} + + ); +} + +export function ListGroupHeaderTitle(props: Omit, 'elementDescriptor'>) { + return ( + + ); +} + +export function ListGroupContent({ + children, + sx, + ...props +}: Omit, 'as' | 'elementDescriptor'>) { + return ( + ({ margin: t.sizes.$none, padding: t.sizes.$none }), sx]} + elementDescriptor={descriptors.listGroupContent} + > + {children} + + ); +} + +export function ListGroupItem({ + children, + sx, + ...props +}: Omit, 'as' | 'elementDescriptor'>) { + return ( + ({ + 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', + width: t.space.$1, + height: t.space.$1, + background: t.colors.$colorMutedForeground, + borderRadius: t.radii.$circle, + transform: 'translateY(-0.1875rem)', + marginInlineEnd: t.space.$2, + flexShrink: 0, + }, + }), + sx, + ]} + elementDescriptor={descriptors.listGroupItem} + > + {children} + + ); +} + +export function ListGroupItemLabel(props: Omit, 'elementDescriptor'>) { + return ( + + ); +} diff --git a/packages/ui/src/components/OAuthConsent/LogoGroup.tsx b/packages/ui/src/components/OAuthConsent/LogoGroup.tsx new file mode 100644 index 00000000000..6ab54c0348d --- /dev/null +++ b/packages/ui/src/components/OAuthConsent/LogoGroup.tsx @@ -0,0 +1,100 @@ +import { descriptors, Flex } from '@/ui/customizables'; +import type { ComponentProps } from 'react'; +import type { ThemableCssProp } from '@/ui/styledSystem'; +import { Box, Icon } from '@/ui/customizables'; +import { LockDottedCircle } from '@/ui/icons'; +import { colors } from '@/ui/utils/colors'; +import { common } from '@/ui/styledSystem'; + +export function LogoGroup({ children }: { children: React.ReactNode }) { + return ( + ({ + marginBlockEnd: t.space.$6, + })} + elementDescriptor={descriptors.logoGroup} + > + {children} + + ); +} + +export function LogoGroupItem({ children, sx, ...props }: ComponentProps) { + return ( + + {children} + + ); +} + +export function LogoGroupIcon({ size = 'md', sx }: { size?: 'sm' | 'md'; sx?: ThemableCssProp }) { + const scale: ThemableCssProp = t => { + const value = size === 'sm' ? t.space.$6 : t.space.$12; + return { + width: value, + height: value, + }; + }; + + return ( + [ + { + background: common.mergedColorsBackground( + colors.setAlpha(t.colors.$colorBackground, 1), + t.colors.$neutralAlpha50, + ), + borderRadius: t.radii.$circle, + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$borderAlpha100, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + scale, + sx, + ]} + elementDescriptor={descriptors.logoGroupIcon} + > + ({ + color: t.colors.$primary500, + })} + /> + + ); +} + +export function LogoGroupSeparator() { + return ( + ({ + color: t.colors.$colorMutedForeground, + })} + elementDescriptor={descriptors.logoGroupSeparator} + > + + + ); +} diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index 0fcf18e1e20..bc610b673a5 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -1,22 +1,27 @@ import { useUser } from '@clerk/shared/react'; -import type { ComponentProps } from 'react'; import { useState } from 'react'; import { useEnvironment, useOAuthConsentContext } from '@/ui/contexts'; -import { Box, Button, Flex, Flow, Grid, Icon, Text } from '@/ui/customizables'; +import { Box, Button, Flow, Grid, Text } from '@/ui/customizables'; import { ApplicationLogo } from '@/ui/elements/ApplicationLogo'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { Modal } from '@/ui/elements/Modal'; import { Tooltip } from '@/ui/elements/Tooltip'; -import { LockDottedCircle } from '@/ui/icons'; import { Alert, Textarea } from '@/ui/primitives'; -import type { ThemableCssProp } from '@/ui/styledSystem'; import { common } from '@/ui/styledSystem'; import { colors } from '@/ui/utils/colors'; - +import { LogoGroup, LogoGroupItem, LogoGroupIcon, LogoGroupSeparator } from './LogoGroup'; import { OrgSelect } from './OrgSelect'; +import { + ListGroup, + ListGroupContent, + ListGroupHeader, + ListGroupHeaderTitle, + ListGroupItem, + ListGroupItemLabel, +} from './ListGroup'; const OFFLINE_ACCESS_SCOPE = 'offline_access'; @@ -61,24 +66,24 @@ export function OAuthConsentInternal() { {/* both have avatars */} {oAuthApplicationLogoUrl && logoImageUrl && ( - - + + - - - + + + - - + + )} {/* only OAuth app has an avatar */} {oAuthApplicationLogoUrl && !logoImageUrl && ( - + - ({ position: 'absolute', @@ -99,101 +104,51 @@ export function OAuthConsentInternal() { })} /> - + )} {/* only Clerk application has an avatar */} {!oAuthApplicationLogoUrl && logoImageUrl && ( - - - - - - + + + + + + - - + + )} {/* no avatars */} {!oAuthApplicationLogoUrl && !logoImageUrl && ( - - - + + + )} {selectOptions.length > 0 && ( - - - + )} - ({ - textAlign: 'start', - borderWidth: t.borderWidths.$normal, - borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha100, - borderRadius: t.radii.$lg, - overflow: 'hidden', - })} - > - ({ - padding: t.space.$3, - background: common.mergedColorsBackground( - colors.setAlpha(t.colors.$colorBackground, 1), - t.colors.$neutralAlpha50, - ), - })} - > - - - ({ margin: t.sizes.$none, padding: t.sizes.$none })} - > + + + + + {displayedScopes.map(item => ( - ({ - 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', - width: t.space.$1, - height: t.space.$1, - background: t.colors.$colorMutedForeground, - borderRadius: t.radii.$circle, - transform: 'translateY(-0.1875rem)', - marginInlineEnd: t.space.$2, - flexShrink: 0, - }, - })} - as='li' - > - - + + {item.description || item.scope || ''} + ))} - - + + + ({ - marginBlockEnd: t.space.$6, - })} - > - {children} - - ); -} - -function ConnectionItem({ children, sx, ...props }: ComponentProps) { - return ( - - {children} - - ); -} - -function ConnectionIcon({ size = 'md', sx }: { size?: 'sm' | 'md'; sx?: ThemableCssProp }) { - const scale: ThemableCssProp = t => { - const value = size === 'sm' ? t.space.$6 : t.space.$12; - return { - width: value, - height: value, - }; - }; - - return ( - [ - { - background: common.mergedColorsBackground( - colors.setAlpha(t.colors.$colorBackground, 1), - t.colors.$neutralAlpha50, - ), - borderRadius: t.radii.$circle, - borderWidth: t.borderWidths.$normal, - borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$borderAlpha100, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, - scale, - sx, - ]} - > - ({ - color: t.colors.$primary500, - })} - /> - - ); -} - -function ConnectionSeparator() { - return ( - ({ - color: t.colors.$colorMutedForeground, - })} - > - - - ); -} - export const OAuthConsent = withCardStateProvider(OAuthConsentInternal); diff --git a/packages/ui/src/components/OAuthConsent/OrgSelect.tsx b/packages/ui/src/components/OAuthConsent/OrgSelect.tsx index 10cba35f52f..ce7cfe3ca95 100644 --- a/packages/ui/src/components/OAuthConsent/OrgSelect.tsx +++ b/packages/ui/src/components/OAuthConsent/OrgSelect.tsx @@ -78,6 +78,7 @@ export function OrgSelect({ options, value, onChange }: OrgSelectProps) { sx={theme => ({ inlineSize: 'min(100%, 16rem)', paddingInline: theme.space.$3, + marginInline: 'auto', })} > Date: Fri, 10 Apr 2026 15:21:32 -0400 Subject: [PATCH 6/6] chore(ui): Add OauthConsent localizations pt 1 (#8284) --- packages/localizations/src/en-US.ts | 17 +++ packages/shared/src/types/localization.ts | 16 +++ .../components/OAuthConsent/InlineAction.tsx | 83 ++++++++++++ .../components/OAuthConsent/OAuthConsent.tsx | 101 +++++++-------- .../__tests__/InlineAction.test.tsx | 119 ++++++++++++++++++ 5 files changed, 277 insertions(+), 59 deletions(-) create mode 100644 packages/ui/src/components/OAuthConsent/InlineAction.tsx create mode 100644 packages/ui/src/components/OAuthConsent/__tests__/InlineAction.test.tsx diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 70ea6c728fa..3b5b3e1bc61 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -294,6 +294,23 @@ export const enUS: LocalizationResource = { title: 'Choose an account', titleWithoutPersonal: 'Choose an organization', }, + oauthConsent: { + action__allow: 'Allow', + action__deny: 'Deny', + offlineAccessNotice: " You'll stay signed in until you sign out or revoke access.", + redirectNotice: 'If you allow access, this app will redirect you to {{domainAction}}.', + redirectUriModal: { + subtitle: 'Make sure you trust {{applicationName}} and that this URL belongs to {{applicationName}}.', + title: 'Redirect URL', + }, + scopeList: { + title: 'This will allow {{applicationName}} access to:', + }, + subtitle: 'wants to access {{applicationName}} on behalf of {{identifier}}', + viewFullUrl: 'View full URL', + warning: + 'Make sure that you trust {{applicationName}} ({{domainAction}}). You may be sharing sensitive data with this site or app.', + }, organizationProfile: { apiKeysPage: { title: 'API keys', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 5e65b15004e..094ac9cdb5b 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1252,6 +1252,22 @@ export type __internal_LocalizationResource = { suggestionsAcceptedLabel: LocalizationValue; action__createOrganization: LocalizationValue; }; + oauthConsent: { + subtitle: LocalizationValue<'applicationName' | 'identifier'>; + scopeList: { + title: LocalizationValue<'applicationName'>; + }; + action__deny: LocalizationValue; + action__allow: LocalizationValue; + warning: LocalizationValue<'applicationName' | 'domainAction'>; + redirectNotice: LocalizationValue<'domainAction'>; + offlineAccessNotice: LocalizationValue; + viewFullUrl: LocalizationValue; + redirectUriModal: { + title: LocalizationValue; + subtitle: LocalizationValue<'applicationName'>; + }; + }; unstable__errors: UnstableErrors; dates: { previous6Days: LocalizationValue<'date'>; diff --git a/packages/ui/src/components/OAuthConsent/InlineAction.tsx b/packages/ui/src/components/OAuthConsent/InlineAction.tsx new file mode 100644 index 00000000000..b8361d17cfc --- /dev/null +++ b/packages/ui/src/components/OAuthConsent/InlineAction.tsx @@ -0,0 +1,83 @@ +import React from 'react'; + +import { Text } from '@/ui/customizables'; +import { Tooltip } from '@/ui/elements/Tooltip'; + +type InlineActionProps = { + text: string; + actionText: string; + onClick: () => void; + tooltipText: string; +}; + +export function InlineAction({ text, actionText, onClick, tooltipText }: InlineActionProps) { + const idx = text.indexOf(actionText); + if (idx === -1) { + return <>{text}; + } + + let before = text.slice(0, idx); + let after = text.slice(idx + actionText.length); + + // Pull adjacent parentheses into the action span so they don't wrap separately + let prefix = ''; + let suffix = ''; + if (before.endsWith('(')) { + before = before.slice(0, -1); + prefix = '('; + } + if (after.startsWith(')')) { + after = after.slice(1); + suffix = ')'; + } + + const actionContent = ( + + + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }} + sx={{ + textDecoration: 'underline', + textDecorationStyle: 'dotted', + cursor: 'pointer', + outline: 'none', + display: 'inline-block', + }} + > + {actionText} + + + + + ); + + return ( + <> + {before} + {prefix || suffix ? ( + + {prefix} + {actionContent} + {suffix} + + ) : ( + actionContent + )} + {after} + + ); +} diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index bc610b673a5..8df909f4a78 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -2,16 +2,14 @@ import { useUser } from '@clerk/shared/react'; import { useState } from 'react'; import { useEnvironment, useOAuthConsentContext } from '@/ui/contexts'; -import { Box, Button, Flow, Grid, Text } from '@/ui/customizables'; +import { Box, Button, Flow, Grid, localizationKeys, Text, useLocalizations } from '@/ui/customizables'; import { ApplicationLogo } from '@/ui/elements/ApplicationLogo'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { Modal } from '@/ui/elements/Modal'; -import { Tooltip } from '@/ui/elements/Tooltip'; import { Alert, Textarea } from '@/ui/primitives'; -import { common } from '@/ui/styledSystem'; -import { colors } from '@/ui/utils/colors'; +import { InlineAction } from './InlineAction'; import { LogoGroup, LogoGroupItem, LogoGroupIcon, LogoGroupSeparator } from './LogoGroup'; import { OrgSelect } from './OrgSelect'; import { @@ -55,10 +53,14 @@ export function OAuthConsentInternal() { const { hostname } = new URL(redirectUrl); return hostname.split('.').slice(-2).join('.'); } catch { - return ''; + return 'https://example.com'; } } + const { t } = useLocalizations(); + const domainAction = getRootDomain(); + const viewFullUrlText = t(localizationKeys('oauthConsent.viewFullUrl')); + return ( @@ -125,7 +127,12 @@ export function OAuthConsentInternal() { )} - + {selectOptions.length > 0 && ( @@ -138,7 +145,11 @@ export function OAuthConsentInternal() { - + {displayedScopes.map(item => ( @@ -154,30 +165,17 @@ export function OAuthConsentInternal() { colorScheme='warning' variant='caption' > - Make sure that you trust {oAuthApplicationName} {''} - - - setIsUriModalOpen(true)} - > - ({getRootDomain()}) - - - - - {''}. You may be sharing sensitive data with this site or app. + setIsUriModalOpen(true)} + tooltipText={viewFullUrlText} + />