-
Notifications
You must be signed in to change notification settings - Fork 392
feat(clerk-js,types,localizations): Choose enterprise connection on sign-in/sign-up #6947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
71e940f
eb91b6e
0685ba6
030015b
bd48187
9d2e414
031fa4c
c33ba36
c899e3b
d2494ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@clerk/localizations': minor | ||
'@clerk/clerk-js': minor | ||
'@clerk/types': minor | ||
--- | ||
|
||
Introduce experimental step to choose enterprise connection on sign-in/sign-up |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { useState } from 'react'; | ||
|
||
import type { LocalizationKey } from '@/ui/customizables'; | ||
import { descriptors, Flex, Grid, SimpleButton, Spinner, Text } from '@/ui/customizables'; | ||
import { Card } from '@/ui/elements/Card'; | ||
import { useCardState } from '@/ui/elements/contexts'; | ||
import { Header } from '@/ui/elements/Header'; | ||
import type { InternalTheme, PropsOfComponent } from '@/ui/styledSystem'; | ||
|
||
type ChooseEnterpriseConnectionCardProps = { | ||
title: LocalizationKey; | ||
subtitle: LocalizationKey; | ||
onClick: (id: string) => Promise<void>; | ||
enterpriseConnections: Array<{ id: string; name: string }>; | ||
}; | ||
|
||
/** | ||
* @experimental | ||
*/ | ||
export const ChooseEnterpriseConnectionCard = ({ | ||
title, | ||
subtitle, | ||
onClick, | ||
enterpriseConnections, | ||
}: ChooseEnterpriseConnectionCardProps) => { | ||
const card = useCardState(); | ||
LauraBeatris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return ( | ||
<Card.Root> | ||
<Card.Content> | ||
<Header.Root showLogo> | ||
<Header.Title localizationKey={title} /> | ||
<Header.Subtitle localizationKey={subtitle} /> | ||
</Header.Root> | ||
<Card.Alert>{card.error}</Card.Alert> | ||
|
||
<Grid | ||
elementDescriptor={descriptors.enterpriseConnectionsRoot} | ||
gap={2} | ||
> | ||
{enterpriseConnections?.map(({ id, name }) => ( | ||
<ChooseEnterpriseConnectionButton | ||
key={id} | ||
id={id} | ||
label={name} | ||
onClick={onClick} | ||
/> | ||
))} | ||
</Grid> | ||
</Card.Content> | ||
|
||
<Card.Footer /> | ||
</Card.Root> | ||
); | ||
}; | ||
|
||
type ChooseEnterpriseConnectionButtonProps = Omit<PropsOfComponent<typeof SimpleButton>, 'onClick'> & { | ||
id: string; | ||
label?: string; | ||
onClick: (id: string) => Promise<void>; | ||
}; | ||
|
||
const ChooseEnterpriseConnectionButton = (props: ChooseEnterpriseConnectionButtonProps): JSX.Element => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any way to reuse the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought of not reusing since this is an experimental UI, and I wanted to minimize the changes for the core flows. We could extract |
||
const { label, onClick, ...rest } = props; | ||
const [isLoading, setIsLoading] = useState(false); | ||
|
||
const handleClick = () => { | ||
setIsLoading(true); | ||
void onClick(props.id).catch(() => setIsLoading(false)); | ||
}; | ||
|
||
return ( | ||
<SimpleButton | ||
elementDescriptor={descriptors.enterpriseConnectionButton} | ||
variant='outline' | ||
block | ||
isLoading={isLoading} | ||
hoverAsFocus | ||
onClick={handleClick} | ||
{...rest} | ||
sx={(theme: InternalTheme) => [ | ||
{ | ||
gap: theme.space.$4, | ||
position: 'relative', | ||
justifyContent: 'flex-start', | ||
}, | ||
(rest as any).sx, | ||
]} | ||
> | ||
<Flex | ||
justify='center' | ||
align='center' | ||
as='span' | ||
gap={3} | ||
sx={{ | ||
width: '100%', | ||
overflow: 'hidden', | ||
}} | ||
> | ||
{isLoading && ( | ||
<Flex | ||
as='span' | ||
center | ||
sx={(theme: InternalTheme) => ({ flex: `0 0 ${theme.space.$4}` })} | ||
> | ||
<Spinner | ||
size='sm' | ||
elementDescriptor={descriptors.spinner} | ||
/> | ||
</Flex> | ||
)} | ||
<Text | ||
elementDescriptor={descriptors.enterpriseConnectionButtonText} | ||
as='span' | ||
truncate | ||
variant='buttonLarge' | ||
> | ||
{label} | ||
</Text> | ||
</Flex> | ||
</SimpleButton> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { useClerk } from '@clerk/shared/react/index'; | ||
import type { ComponentType } from 'react'; | ||
|
||
import { buildSSOCallbackURL, withRedirect } from '@/ui/common'; | ||
import { ChooseEnterpriseConnectionCard } from '@/ui/common/ChooseEnterpriseConnectionCard'; | ||
import { useCoreSignIn, useEnvironment, useSignInContext } from '@/ui/contexts'; | ||
import { Flow, localizationKeys } from '@/ui/customizables'; | ||
import { withCardStateProvider } from '@/ui/elements/contexts'; | ||
import type { AvailableComponentProps } from '@/ui/types'; | ||
|
||
import { hasMultipleEnterpriseConnections } from './shared'; | ||
|
||
/** | ||
* @experimental | ||
*/ | ||
const SignInFactorOneEnterpriseConnectionsInternal = () => { | ||
const ctx = useSignInContext(); | ||
const { displayConfig } = useEnvironment(); | ||
|
||
const clerk = useClerk(); | ||
const signIn = clerk.client.signIn; | ||
|
||
if (!hasMultipleEnterpriseConnections(signIn.supportedFirstFactors)) { | ||
// This should not happen due to the HOC guard, but provides type safety | ||
return null; | ||
} | ||
LauraBeatris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const enterpriseConnections = signIn.supportedFirstFactors.map(ff => ({ | ||
id: ff.enterpriseConnectionId, | ||
name: ff.enterpriseConnectionName, | ||
})); | ||
|
||
const handleEnterpriseSSO = (enterpriseConnectionId: string) => { | ||
const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signInUrl); | ||
const redirectUrlComplete = ctx.afterSignInUrl || '/'; | ||
|
||
return signIn.authenticateWithRedirect({ | ||
strategy: 'enterprise_sso', | ||
redirectUrl, | ||
redirectUrlComplete, | ||
oidcPrompt: ctx.oidcPrompt, | ||
continueSignIn: true, | ||
enterpriseConnectionId, | ||
}); | ||
}; | ||
LauraBeatris marked this conversation as resolved.
Show resolved
Hide resolved
LauraBeatris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return ( | ||
<Flow.Part part='enterpriseConnections'> | ||
<ChooseEnterpriseConnectionCard | ||
title={localizationKeys('signIn.enterpriseConnections.title')} | ||
subtitle={localizationKeys('signIn.enterpriseConnections.subtitle')} | ||
onClick={handleEnterpriseSSO} | ||
enterpriseConnections={enterpriseConnections} | ||
/> | ||
</Flow.Part> | ||
); | ||
}; | ||
|
||
const withEnterpriseConnectionsGuard = <P extends AvailableComponentProps>(Component: ComponentType<P>) => { | ||
const displayName = Component.displayName || Component.name || 'Component'; | ||
Component.displayName = displayName; | ||
|
||
const HOC = (props: P) => { | ||
const signIn = useCoreSignIn(); | ||
const signInCtx = useSignInContext(); | ||
|
||
return withRedirect( | ||
Component, | ||
() => !hasMultipleEnterpriseConnections(signIn.supportedFirstFactors), | ||
({ clerk }) => signInCtx.signInUrl || clerk.buildSignInUrl(), | ||
'There are no enterprise connections available to sign-in. Clerk is redirecting to the `signInUrl` instead.', | ||
)(props); | ||
LauraBeatris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
HOC.displayName = `withEnterpriseConnectionsGuard(${displayName})`; | ||
|
||
return HOC; | ||
}; | ||
|
||
export const SignInFactorOneEnterpriseConnections = withCardStateProvider( | ||
withEnterpriseConnectionsGuard(SignInFactorOneEnterpriseConnectionsInternal), | ||
); |
Uh oh!
There was an error while loading. Please reload this page.