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/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 496eddb787a..8df909f4a78 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -1,20 +1,25 @@ 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, 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 { 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 { InlineAction } from './InlineAction'; +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'; @@ -24,6 +29,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; @@ -36,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 ( @@ -47,24 +68,24 @@ export function OAuthConsentInternal() { {/* both have avatars */} {oAuthApplicationLogoUrl && logoImageUrl && ( - - + + - - - + + + - - + + )} {/* only OAuth app has an avatar */} {oAuthApplicationLogoUrl && !logoImageUrl && ( - + - ({ position: 'absolute', @@ -85,119 +106,76 @@ export function OAuthConsentInternal() { })} /> - + )} {/* only Clerk application has an avatar */} {!oAuthApplicationLogoUrl && logoImageUrl && ( - - - - - - + + + + + + - - + + )} {/* no avatars */} {!oAuthApplicationLogoUrl && !logoImageUrl && ( - - - + + + )} - - - ({ - 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, - ), + - + + + {selectOptions.length > 0 && ( + + )} + + + + - - ({ 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 || ''} + ))} - - + + + - Make sure that you trust {oAuthApplicationName} {''} - - - setIsUriModalOpen(true)} - > - ({getRootDomain()}) - - - - - {''}. You may be sharing sensitive data with this site or app. + setIsUriModalOpen(true)} + tooltipText={viewFullUrlText} + />