Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
c201042023d9afda46a7c1a5e8c7972d352029c44f2022081a3ab014551b23a78d6cc4a7f1340File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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} | ||||
Contributor 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 constAuthenticatedContent=withCoreUserGuard(()=>{const{ data, isLoading }=__internal_useUserEnterpriseConnections({enabled: true});if(isLoading&&!data){return<FullSpinner/>;}return(// …existing layout…<ConfigureSSOFlowProviderenterpriseConnection={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 */} | ||||
Contributor 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'> | ||
Contributor 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> | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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