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} + />