-
Notifications
You must be signed in to change notification settings - Fork 453
chore(ui): Add wizard steps to <ConfigureSSO />
#8468
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
c201042
023d9af
da46a7c
1a5e8c7
972d352
029c44f
2022081
a3ab014
551b23a
78d6cc4
a7f1340
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,5 @@ | ||
| --- | ||
| '@clerk/ui': patch | ||
| --- | ||
|
|
||
| Add wizard steps for the `<__experimental_ConfigureSSO />` component |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -1,26 +1,28 @@ | ||||
| import { useOrganization } from '@clerk/shared/react/index'; | ||||
| import { __internal_useUserEnterpriseConnections, useOrganization, useUser } from '@clerk/shared/react'; | ||||
| import type { __experimental_ConfigureSSOProps } from '@clerk/shared/types'; | ||||
| import React from 'react'; | ||||
|
|
||||
| import { useEnvironment, withCoreUserGuard } from '@/contexts'; | ||||
| import { Box, Col, Flex, Flow, Icon, localizationKeys, Text, useAppearance } from '@/customizables'; | ||||
| import { Box, Col, descriptors, Flex, Flow, Icon, localizationKeys, Text, useAppearance } from '@/customizables'; | ||||
| import { ApplicationLogo } from '@/elements/ApplicationLogo'; | ||||
| import { withCardStateProvider } from '@/elements/contexts'; | ||||
| import { NavBar, NavbarContextProvider } from '@/elements/Navbar'; | ||||
| import { ProfileCard } from '@/elements/ProfileCard'; | ||||
| import { BoxIcon } from '@/icons'; | ||||
| import { Route, Switch } from '@/router'; | ||||
|
|
||||
| import { ConfigureSSOFlowProvider } from './ConfigureSSOContext'; | ||||
| import { ConfigureCreateApp, ConfirmationStep, ProvideEmail, TestConfigurationStep, VerifyDomainStep } from './steps'; | ||||
| import { ConfigureSSOWizard } from './wizard'; | ||||
|
|
||||
| const ConfigureSSOInternal = () => { | ||||
| return ( | ||||
| <Flow.Root flow='configureSSO'> | ||||
| <Flow.Part> | ||||
| <Switch> | ||||
| <Route> | ||||
| <AuthenticatedContent /> | ||||
| </Route> | ||||
| </Switch> | ||||
| </Flow.Part> | ||||
| <Switch> | ||||
| <Route> | ||||
| <AuthenticatedContent /> | ||||
| </Route> | ||||
| </Switch> | ||||
| </Flow.Root> | ||||
| ); | ||||
| }; | ||||
|
|
@@ -32,6 +34,11 @@ const AuthenticatedContent = withCoreUserGuard(() => { | |||
| const { parsedOptions } = useAppearance(); | ||||
| const hasLogo = Boolean(parsedOptions.logoImageUrl || logoImageUrl); | ||||
|
|
||||
| const { data: enterpriseConnections, isLoading: isLoadingEnterpriseConnections } = | ||||
| __internal_useUserEnterpriseConnections({ enabled: true }); | ||||
| // Currently FAPI only supports one enterprise connection per user | ||||
| const enterpriseConnection = enterpriseConnections?.[0]; | ||||
|
|
||||
| return ( | ||||
| <ProfileCard.Root | ||||
| sx={t => ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })} | ||||
|
|
@@ -89,12 +96,97 @@ const AuthenticatedContent = withCoreUserGuard(() => { | |||
| routes={[]} | ||||
| contentRef={contentRef} | ||||
| /> | ||||
| <ProfileCard.Content contentRef={contentRef} /> | ||||
| <Col | ||||
| ref={contentRef} | ||||
| elementDescriptor={descriptors.scrollBox} | ||||
| sx={t => ({ | ||||
| backgroundColor: t.colors.$colorBackground, | ||||
| position: 'relative', | ||||
| borderRadius: t.radii.$lg, | ||||
| width: '100%', | ||||
| overflow: 'hidden', | ||||
| borderWidth: t.borderWidths.$normal, | ||||
| borderStyle: t.borderStyles.$solid, | ||||
| borderColor: t.colors.$borderAlpha150, | ||||
| marginBlock: '-1px', | ||||
| marginInlineEnd: '-1px', | ||||
| flex: 1, | ||||
| })} | ||||
| > | ||||
| <ConfigureSSOFlowProvider | ||||
| enterpriseConnection={enterpriseConnection} | ||||
| isLoading={isLoadingEnterpriseConnections} | ||||
|
Member
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 think passing the initial const AuthenticatedContent = withCoreUserGuard(() => {
const { data, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });
if (isLoading && !data) {
return <FullSpinner />;
}
return (
// …existing layout…
<ConfigureSSOFlowProvider enterpriseConnection={data?.[0]}>
<ConfigureSSOSteps />
</ConfigureSSOFlowProvider>
);
});Happy to take this as a separate PR if you'd rather keep this one tight. |
||||
| > | ||||
| <ConfigureSSOSteps /> | ||||
| </ConfigureSSOFlowProvider> | ||||
| </Col> | ||||
| </NavbarContextProvider> | ||||
| </ProfileCard.Root> | ||||
| ); | ||||
| }); | ||||
|
|
||||
| const ConfigureSSOSteps = () => { | ||||
| const { user } = useUser(); | ||||
|
|
||||
| const primaryEmailAddress = user?.primaryEmailAddress; | ||||
|
|
||||
| return ( | ||||
| <ConfigureSSOWizard> | ||||
| <ConfigureSSOWizard.Step | ||||
| id='verify-email-domain' | ||||
| path='verify-email-domain' | ||||
| label='Verify domain' | ||||
| > | ||||
| <ConfigureSSOWizard> | ||||
| {!primaryEmailAddress && ( | ||||
| <ConfigureSSOWizard.Step | ||||
| id='provide-email' | ||||
| path='provide-email' | ||||
| > | ||||
| <ProvideEmail /> | ||||
| </ConfigureSSOWizard.Step> | ||||
| )} | ||||
| <ConfigureSSOWizard.Step | ||||
| id='verify-domain' | ||||
| path='verify-domain' | ||||
| > | ||||
| <VerifyDomainStep /> | ||||
| </ConfigureSSOWizard.Step> | ||||
| </ConfigureSSOWizard> | ||||
| </ConfigureSSOWizard.Step> | ||||
| <ConfigureSSOWizard.Step | ||||
| id='configure' | ||||
| path='configure' | ||||
| label='Configure' | ||||
| > | ||||
| <ConfigureSSOWizard> | ||||
| {/* TODO: Implement configure steps */} | ||||
|
Member
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.
Suggested change
|
||||
| <ConfigureSSOWizard.Step | ||||
| id='create-app' | ||||
| path='create-app' | ||||
| > | ||||
| <ConfigureCreateApp /> | ||||
| </ConfigureSSOWizard.Step> | ||||
| </ConfigureSSOWizard> | ||||
| </ConfigureSSOWizard.Step> | ||||
| <ConfigureSSOWizard.Step | ||||
| id='test' | ||||
| path='test' | ||||
| label='Test' | ||||
| > | ||||
| <TestConfigurationStep /> | ||||
| </ConfigureSSOWizard.Step> | ||||
| <ConfigureSSOWizard.Step | ||||
| id='confirmation' | ||||
| path='confirmation' | ||||
| label='Confirmation' | ||||
| > | ||||
| <ConfirmationStep /> | ||||
| </ConfigureSSOWizard.Step> | ||||
| </ConfigureSSOWizard> | ||||
| ); | ||||
| }; | ||||
|
|
||||
| const OrganizationSidebarSubtitle = () => { | ||||
| const { organization } = useOrganization(); | ||||
|
|
||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { EnterpriseConnectionResource } from '@clerk/shared/types'; | ||
| import React, { type PropsWithChildren } from 'react'; | ||
|
|
||
| /** | ||
| * Shared form state for the ConfigureSSO wizard, persisted across steps | ||
| */ | ||
| export interface ConfigureSSOData { | ||
| /** | ||
| * The enterprise connection from the user's primary email address domain | ||
| */ | ||
| enterpriseConnection: EnterpriseConnectionResource | undefined; | ||
| } | ||
|
|
||
| export interface ConfigureSSOContextValue extends ConfigureSSOData { | ||
| /** | ||
| * `true` while the parent is still fetching the user's enterprise | ||
| * connection | ||
| */ | ||
| isLoading: boolean; | ||
| } | ||
|
|
||
| interface ConfigureSSOFlowProviderProps { | ||
| enterpriseConnection: EnterpriseConnectionResource | undefined; | ||
| isLoading: boolean; | ||
| } | ||
|
|
||
| const ConfigureSSOFlowContext = React.createContext<ConfigureSSOContextValue | null>(null); | ||
| ConfigureSSOFlowContext.displayName = 'ConfigureSSOFlowContext'; | ||
|
|
||
| export const ConfigureSSOFlowProvider = ({ | ||
| enterpriseConnection, | ||
| isLoading, | ||
| children, | ||
| }: PropsWithChildren<ConfigureSSOFlowProviderProps>): JSX.Element => { | ||
| const value = React.useMemo<ConfigureSSOContextValue>( | ||
| () => ({ | ||
| enterpriseConnection, | ||
| isLoading, | ||
| }), | ||
| [enterpriseConnection, isLoading], | ||
| ); | ||
|
|
||
| return <ConfigureSSOFlowContext.Provider value={value}>{children}</ConfigureSSOFlowContext.Provider>; | ||
| }; | ||
|
|
||
| export const useConfigureSSOFlow = (): ConfigureSSOContextValue => { | ||
| const ctx = React.useContext(ConfigureSSOFlowContext); | ||
| if (!ctx) { | ||
| throw new Error('useConfigureSSOFlow called outside <ConfigureSSOFlowProvider>.'); | ||
| } | ||
| return ctx; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { Flow, Text } from '@/customizables'; | ||
|
|
||
| import { useConfigureSSOWizard, useRegisterContinueAction } from '../wizard'; | ||
| import { StepLayout } from './StepLayout'; | ||
|
|
||
| export const ConfigureCreateApp = (): JSX.Element => { | ||
| const { goNext } = useConfigureSSOWizard(); | ||
|
|
||
| useRegisterContinueAction({ | ||
| handler: () => goNext(), | ||
| }); | ||
|
|
||
| return ( | ||
| <Flow.Part part='configureCreateApp'> | ||
| <StepLayout | ||
| title='Configure Okta Workforce' | ||
| subtitle='Create a new enterprise application in your Okta Dashboard.' | ||
| > | ||
| <Text>UI goes here</Text> | ||
| </StepLayout> | ||
| </Flow.Part> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { Flow, Text } from '@/customizables'; | ||
|
|
||
| import { StepLayout } from './StepLayout'; | ||
|
|
||
| export const ConfirmationStep = (): JSX.Element => { | ||
| return ( | ||
| <Flow.Part part='sso-confirmation'> | ||
| <StepLayout> | ||
| <Text>UI goes here</Text> | ||
| </StepLayout> | ||
| </Flow.Part> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { Flow, Text } from '@/customizables'; | ||
|
|
||
| import { useConfigureSSOWizard, useRegisterContinueAction } from '../wizard'; | ||
| import { StepLayout } from './StepLayout'; | ||
|
|
||
| export const ProvideEmail = (): JSX.Element => { | ||
| const { goNext } = useConfigureSSOWizard(); | ||
|
|
||
| useRegisterContinueAction({ | ||
| handler: () => { | ||
| return goNext(); | ||
| }, | ||
| }); | ||
|
|
||
| return ( | ||
| <Flow.Part part='provideEmail'> | ||
| <StepLayout | ||
| title='Verify your domain' | ||
| subtitle='Verify the domain you want to enable the enterprise connection on.' | ||
| > | ||
| <Text as='p'>UI goes here</Text> | ||
| </StepLayout> | ||
| </Flow.Part> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import React from 'react'; | ||
|
|
||
| import { Col, Flex, Heading, Text } from '@/customizables'; | ||
|
|
||
| import { ConfigureSSOWizard } from '../wizard'; | ||
|
|
||
| interface StepLayoutProps { | ||
| title?: React.ReactNode; | ||
| subtitle?: React.ReactNode; | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| /** | ||
| * Renders the title row (with the Wizard's Step X/Y badge) on top, a divider, and the step body | ||
| * underneath. Each individual step file owns the body content | ||
| * | ||
| * The Step X/Y badge is rendered via `ConfigureSSOWizard.StepIndicator`, | ||
| * which self-hides on steps that have no inner sub-steps | ||
| */ | ||
| export const StepLayout = ({ title, subtitle, children }: StepLayoutProps): JSX.Element => { | ||
| return ( | ||
| <Col | ||
| sx={{ | ||
| flex: 1, | ||
| minHeight: 0, | ||
| }} | ||
| > | ||
| <Flex | ||
| align='center' | ||
| justify='between' | ||
| sx={theme => ({ | ||
| gap: theme.space.$4, | ||
| padding: theme.space.$5, | ||
| })} | ||
| > | ||
| {title ? ( | ||
| <Col sx={theme => ({ gap: theme.space.$1, minWidth: 0 })}> | ||
| <Heading | ||
| textVariant='h3' | ||
| sx={theme => ({ color: theme.colors.$colorForeground, fontSize: theme.fontSizes.$lg })} | ||
| > | ||
| {title} | ||
| </Heading> | ||
|
|
||
| {subtitle ? ( | ||
| <Text | ||
| as='p' | ||
| variant='body' | ||
| sx={theme => ({ color: theme.colors.$colorMutedForeground })} | ||
| > | ||
| {subtitle} | ||
| </Text> | ||
| ) : null} | ||
| </Col> | ||
| ) : null} | ||
| <ConfigureSSOWizard.StepIndicator /> | ||
| </Flex> | ||
| <Col | ||
| sx={theme => ({ | ||
| flex: 1, | ||
| paddingInline: theme.space.$5, | ||
| overflowY: 'auto', | ||
| })} | ||
| > | ||
| {children} | ||
| </Col> | ||
| </Col> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { Flow, Text } from '@/customizables'; | ||
|
|
||
| import { StepLayout } from './StepLayout'; | ||
|
|
||
| export const TestConfigurationStep = (): JSX.Element => { | ||
| return ( | ||
| <Flow.Part part='test-sso'> | ||
|
Member
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 think the convention for |
||
| <StepLayout | ||
| title='Test your SSO connection' | ||
| subtitle='Test your SSO configuration to verify you can successfully authenticate via your identity provider' | ||
| > | ||
| <Text>UI goes here</Text> | ||
| </StepLayout> | ||
| </Flow.Part> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this one that got missed on the first PR