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}
+ />
- If you allow access, this app will redirect you to{' '}
-
-
- setIsUriModalOpen(true)}
- >
- {getRootDomain()}
-
-
-
-
- .{hasOfflineAccess && " You'll stay signed in until you sign out or revoke access."}
+ setIsUriModalOpen(true)}
+ tooltipText={viewFullUrlText}
+ />
+ {hasOfflineAccess && t(localizationKeys('oauthConsent.offlineAccessNotice'))}
@@ -282,9 +243,11 @@ function RedirectUriModal({ onOpen, onClose, isOpen, redirectUri, oAuthApplicati
-
+