diff --git a/.changeset/strong-moose-retire.md b/.changeset/strong-moose-retire.md new file mode 100644 index 00000000000..359a1b66440 --- /dev/null +++ b/.changeset/strong-moose-retire.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/shared': patch +'@clerk/ui': patch +--- + +Add confirmation dialog for organization domain deletion as part of self-serve SSO diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 43f0667ebf1..a60df798890 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -213,7 +213,6 @@ export const enUS: LocalizationResource = { configureSSO: { confirmation: { configurationSection: { - certificateLabel: 'Certificate', configureAgainLink: 'Configure again', issuerLabel: 'Issuer', ssoUrlLabel: 'Sign on URL', @@ -283,6 +282,8 @@ export const enUS: LocalizationResource = { badge__verified: 'Verified', badge__unverified: 'Unverified', verifiedAtLabel: "Verified on {{ date | shortDate('en-US') }}", + removeButtonTooltip__lastVerifiedDomain: 'At least one verified domain is required to set up SSO.', + removeButtonTooltip__lastVerifiedDomainActive: 'At least one verified domain is required to keep SSO enabled.', txtRecord: { instructions: "Add this TXT record to your DNS provider. We'll verify automatically once the record is live.", typeLabel: 'Type', @@ -290,6 +291,14 @@ export const enUS: LocalizationResource = { valueLabel: 'Value', }, }, + removeDomainDialog: { + title: 'Removing domain', + subtitle__active: + "You're about to remove {{domain}} from this enterprise connection. Users won't be able to sign-in with {{domain}} anymore.", + subtitle__inactive: "You're about to remove {{domain}} from this enterprise connection.", + cancelButton: 'Cancel', + removeButton: 'Remove domain', + }, }, testConfigurationStep: { title: 'Test your SSO connection', @@ -1077,7 +1086,6 @@ export const enUS: LocalizationResource = { badge__inactive: 'Inactive', badge__inProgress: 'In Progress', badge__unconfigured: 'Unconfigured', - certificateLabel: 'Certificate', descriptionLine1: 'Require members to sign in through your identity provider using their domain email. Members without a matching domain are unaffected.', descriptionLine2: diff --git a/packages/shared/src/react/hooks/useOrganizationDomains.tsx b/packages/shared/src/react/hooks/useOrganizationDomains.tsx index 054a9f388a2..9965b31b6fd 100644 --- a/packages/shared/src/react/hooks/useOrganizationDomains.tsx +++ b/packages/shared/src/react/hooks/useOrganizationDomains.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { logger } from '../../logger'; import type { GetDomainsParams } from '../../types/organization'; @@ -24,6 +24,11 @@ export type UseOrganizationDomainsParams = { * Filter the returned domains by enrollment mode. */ enrollmentMode?: OrganizationEnrollmentMode; + /** + * Invoked from the ownership-verification poll whenever an `attempt` resolves + * one or more domains as `verified`. + */ + onOwnershipVerified?: (verifiedDomains: OrganizationDomainResource[]) => void | Promise; }; export type UseOrganizationDomainsReturn = { @@ -59,11 +64,14 @@ export type UseOrganizationDomainsReturn = { * @internal */ function useOrganizationDomains(params: UseOrganizationDomainsParams = {}): UseOrganizationDomainsReturn { - const { keepPreviousData = true, enabled = true, enrollmentMode } = params; + const { keepPreviousData = true, enabled = true, enrollmentMode, onOwnershipVerified } = params; const clerk = useClerkInstanceContext(); const organization = useOrganizationBase(); const [queryClient] = useClerkQueryClient(); + const onOwnershipVerifiedRef = useRef(onOwnershipVerified); + onOwnershipVerifiedRef.current = onOwnershipVerified; + const { queryKey, stableKey, authenticated } = useOrganizationDomainsCacheKeys({ organizationId: organization?.id ?? null, enrollmentMode, @@ -171,6 +179,14 @@ function useOrganizationDomains(params: UseOrganizationDomainsParams = {}): UseO return; } + const verifiedDomains = result?.data.filter(domain => domain.ownershipVerification?.status === 'verified') ?? []; + if (verifiedDomains.length) { + await onOwnershipVerifiedRef.current?.(verifiedDomains); + } + if (cancelled) { + return; + } + // Stop polling once every domain in the attempt response is verified const allVerified = !!result?.data.length && result.data.every(domain => domain.ownershipVerification?.status === 'verified'); diff --git a/packages/shared/src/types/enterpriseConnection.ts b/packages/shared/src/types/enterpriseConnection.ts index 5e13b860948..0a5636e14d4 100644 --- a/packages/shared/src/types/enterpriseConnection.ts +++ b/packages/shared/src/types/enterpriseConnection.ts @@ -140,7 +140,7 @@ export type MeEnterpriseConnectionOidcInput = OrganizationEnterpriseConnectionOi export type CreateOrganizationEnterpriseConnectionParams = { provider: OrganizationEnterpriseConnectionProvider; - name: string; + name?: string; /** FQDN strings the connection authenticates. Required by the org-scoped create endpoint. */ domains?: string[]; organizationId?: string | null; @@ -153,6 +153,7 @@ export type CreateMeEnterpriseConnectionParams = CreateOrganizationEnterpriseCon export type UpdateOrganizationEnterpriseConnectionParams = { name?: string | null; + domains?: string[]; active?: boolean | null; syncUserAttributes?: boolean | null; disableAdditionalIdentifications?: boolean | null; diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 10227e9be16..c8d8cf0d9fe 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1153,7 +1153,6 @@ export type __internal_LocalizationResource = { domainLabel: LocalizationValue; signOnUrlLabel: LocalizationValue; issuerLabel: LocalizationValue; - certificateLabel: LocalizationValue; menuAction__edit: LocalizationValue; menuAction__activate: LocalizationValue; menuAction__deactivate: LocalizationValue; @@ -1382,6 +1381,8 @@ export type __internal_LocalizationResource = { badge__verified: LocalizationValue; badge__unverified: LocalizationValue; verifiedAtLabel: LocalizationValue<'date'>; + removeButtonTooltip__lastVerifiedDomain: LocalizationValue; + removeButtonTooltip__lastVerifiedDomainActive: LocalizationValue; txtRecord: { instructions: LocalizationValue; typeLabel: LocalizationValue; @@ -1389,6 +1390,13 @@ export type __internal_LocalizationResource = { valueLabel: LocalizationValue; }; }; + removeDomainDialog: { + title: LocalizationValue; + subtitle__active: LocalizationValue<'domain'>; + subtitle__inactive: LocalizationValue<'domain'>; + cancelButton: LocalizationValue; + removeButton: LocalizationValue; + }; }; testConfigurationStep: { title: LocalizationValue; @@ -1835,7 +1843,6 @@ export type __internal_LocalizationResource = { title: LocalizationValue; ssoUrlLabel: LocalizationValue; issuerLabel: LocalizationValue; - certificateLabel: LocalizationValue; configureAgainLink: LocalizationValue; }; resetSection: { diff --git a/packages/ui/src/components/ConfigureSSO/RemoveDomainDialog.tsx b/packages/ui/src/components/ConfigureSSO/RemoveDomainDialog.tsx new file mode 100644 index 00000000000..90644d60e87 --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/RemoveDomainDialog.tsx @@ -0,0 +1,109 @@ +import { useMemo } from 'react'; + +import { Col, descriptors, localizationKeys } from '@/customizables'; +import { Card } from '@/elements/Card'; +import { useCardState, withCardStateProvider } from '@/elements/contexts'; +import { Form } from '@/elements/Form'; +import { FormButtonContainer } from '@/elements/FormButtons'; +import { FormContainer } from '@/elements/FormContainer'; +import { Modal } from '@/elements/Modal'; +import { handleError } from '@/utils/errorHandler'; + +type RemoveDomainDialogProps = { + isOpen: boolean; + onClose: () => void; + domain: string; + isConnectionActive: boolean; + onRemove: () => Promise; + contentRef: React.RefObject; +}; + +export const RemoveDomainDialog = (props: RemoveDomainDialogProps): JSX.Element | null => { + if (!props.isOpen) { + return null; + } + + return ( + ({ + alignItems: 'center', + position: 'absolute', + inset: 0, + width: 'auto', + height: 'auto', + backgroundColor: 'inherit', + backdropFilter: `blur(${t.sizes.$2})`, + })} + > + + + ); +}; + +const RemoveDomainDialogContent = withCardStateProvider((props: RemoveDomainDialogProps) => { + const { onClose, onRemove } = props; + const card = useCardState(); + + const subtitle = useMemo( + () => + props.isConnectionActive + ? localizationKeys('configureSSO.organizationDomainsStep.removeDomainDialog.subtitle__active', { + domain: props.domain, + }) + : localizationKeys('configureSSO.organizationDomainsStep.removeDomainDialog.subtitle__inactive', { + domain: props.domain, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + const onSubmit = async () => { + try { + await onRemove(); + onClose(); + } catch (err) { + handleError(err as Error, [], card.setError); + } + }; + + return ( + ({ borderRadius: t.radii.$md })} + > + ({ textAlign: 'start', padding: t.sizes.$5 })}> + ({ gap: t.space.$4 })} + > + + + + + + + + + + + + ); +}); diff --git a/packages/ui/src/components/ConfigureSSO/__tests__/RemoveDomainDialog.test.tsx b/packages/ui/src/components/ConfigureSSO/__tests__/RemoveDomainDialog.test.tsx new file mode 100644 index 00000000000..3a02bef01ed --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/__tests__/RemoveDomainDialog.test.tsx @@ -0,0 +1,136 @@ +import { ClerkAPIResponseError } from '@clerk/shared/error'; +import { describe, expect, it, vi } from 'vitest'; + +import { bindCreateFixtures } from '@/test/create-fixtures'; +import { render, screen, waitFor } from '@/test/utils'; +import { CardStateProvider } from '@/ui/elements/contexts'; + +import { RemoveDomainDialog } from '../RemoveDomainDialog'; + +const onRemove = vi.fn(); + +const { createFixtures } = bindCreateFixtures('ConfigureSSO'); + +const renderDialog = ( + wrapper: React.ComponentType<{ children?: React.ReactNode }>, + props: { + isOpen?: boolean; + onClose?: () => void; + domain?: string; + isConnectionActive?: boolean; + } = {}, +) => { + const onClose = props.onClose ?? vi.fn(); + const utils = render( + + onRemove()} + contentRef={{ current: null }} + /> + , + { wrapper }, + ); + return { ...utils, onClose }; +}; + +const resetMocks = () => { + onRemove.mockReset(); + onRemove.mockResolvedValue(undefined); +}; + +describe('RemoveDomainDialog', () => { + it('does not render when `isOpen` is `false`', async () => { + resetMocks(); + const { wrapper } = await createFixtures(); + renderDialog(wrapper, { isOpen: false }); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Removing domain' })).not.toBeInTheDocument(); + }); + + it('renders the dialog chrome and actions when isOpen is true', async () => { + resetMocks(); + const { wrapper } = await createFixtures(); + renderDialog(wrapper, { domain: 'acme.com' }); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Removing domain' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Remove domain' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + }); + + it('warns about sign-in impact when the connection is active', async () => { + resetMocks(); + const { wrapper } = await createFixtures(); + renderDialog(wrapper, { domain: 'acme.com', isConnectionActive: true }); + + expect(screen.getByText(/Users won't be able to sign-in with acme\.com anymore/i)).toBeInTheDocument(); + }); + + it('shows the neutral copy when the connection is inactive', async () => { + resetMocks(); + const { wrapper } = await createFixtures(); + renderDialog(wrapper, { domain: 'acme.com', isConnectionActive: false }); + + expect(screen.getByText("You're about to remove acme.com from this enterprise connection.")).toBeInTheDocument(); + expect(screen.queryByText(/Users won't be able to sign-in/i)).not.toBeInTheDocument(); + }); + + it('invokes onClose when Cancel is clicked', async () => { + resetMocks(); + const onClose = vi.fn(); + const { wrapper } = await createFixtures(); + const { userEvent } = renderDialog(wrapper, { onClose }); + + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onClose).toHaveBeenCalledTimes(1); + expect(onRemove).not.toHaveBeenCalled(); + }); + + it('awaits the removal and closes on a successful submit', async () => { + resetMocks(); + const onClose = vi.fn(); + const { wrapper } = await createFixtures(); + const { userEvent } = renderDialog(wrapper, { onClose }); + + await userEvent.click(screen.getByRole('button', { name: 'Remove domain' })); + + await waitFor(() => { + expect(onRemove).toHaveBeenCalledTimes(1); + }); + await waitFor(() => { + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + + it('keeps the dialog open and surfaces an error when removal fails', async () => { + resetMocks(); + onRemove.mockRejectedValueOnce( + new ClerkAPIResponseError('Error', { + data: [ + { + code: 'internal_server_error', + long_message: 'Something went wrong while removing the domain.', + message: 'Removal failed.', + }, + ], + status: 500, + }), + ); + const onClose = vi.fn(); + const { wrapper } = await createFixtures(); + const { userEvent } = renderDialog(wrapper, { onClose }); + + await userEvent.click(screen.getByRole('button', { name: 'Remove domain' })); + + await waitFor(() => { + expect(onRemove).toHaveBeenCalledTimes(1); + }); + expect(await screen.findByText('Something went wrong while removing the domain.')).toBeInTheDocument(); + expect(onClose).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx b/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx index 8b0d8f6be7e..bb9579ad215 100644 --- a/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx +++ b/packages/ui/src/components/ConfigureSSO/hooks/__tests__/useOrganizationEnterpriseConnection.test.tsx @@ -185,7 +185,7 @@ describe('useOrganizationEnterpriseConnection — test-runs gating', () => { }); describe('useOrganizationEnterpriseConnection — mutations', () => { - it('createConnection forwards the provider, the email-derived name, and the organization domain names (no organizationId in body)', async () => { + it('createConnection forwards the provider and the organization domains', async () => { domainsState.data = [{ name: 'acme.com' }, { name: 'example.com' }]; const { result } = renderHook(() => useOrganizationEnterpriseConnection()); @@ -193,12 +193,11 @@ describe('useOrganizationEnterpriseConnection — mutations', () => { await result.current.enterpriseConnectionMutations.createConnection('saml_okta'); expect(mutationSpies.create).toHaveBeenCalledTimes(1); - // `name` is derived from the active user's primary email domain - // (admin@clerk.com → clerk.com); `domains` are the verified organization - // domains passed straight through by the caller. + // `name` is derived by FAPI, so it is not sent from the client; `domains` + // are the verified organization domains passed straight through by the + // caller. expect(mutationSpies.create).toHaveBeenCalledWith({ provider: 'saml_okta', - name: 'clerk.com', domains: ['acme.com', 'example.com'], }); }); @@ -213,7 +212,6 @@ describe('useOrganizationEnterpriseConnection — mutations', () => { expect(mutationSpies.create).toHaveBeenCalledTimes(1); expect(mutationSpies.create).toHaveBeenCalledWith({ provider: 'saml_okta', - name: 'clerk.com', domains: undefined, }); }); diff --git a/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts b/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts index 1bcdf391850..05d390484f4 100644 --- a/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts +++ b/packages/ui/src/components/ConfigureSSO/hooks/useOrganizationEnterpriseConnection.ts @@ -17,7 +17,7 @@ import type { UpdateOrganizationEnterpriseConnectionParams, UserResource, } from '@clerk/shared/types'; -import { useMemo, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { organizationEnterpriseConnection as buildOrganizationEnterpriseConnection, @@ -174,6 +174,24 @@ export const useOrganizationEnterpriseConnection = (): UseOrganizationEnterprise const { session } = useSession(); const { organization } = useOrganization(); + const handleDomainOwnershipVerified = useCallback( + async (verifiedDomains: OrganizationDomainResource[]) => { + if (!enterpriseConnection) { + return; + } + + const verifiedDomainNames = verifiedDomains.map(domain => domain.name); + const domains = Array.from(new Set([...(enterpriseConnection.domains ?? []), ...verifiedDomainNames])); + const hasNewDomains = domains.length !== (enterpriseConnection.domains?.length ?? 0); + if (!hasNewDomains) { + return; + } + + await updateEnterpriseConnection(enterpriseConnection.id, { domains }); + }, + [enterpriseConnection, updateEnterpriseConnection], + ); + const { isLoading: isLoadingOrganizationDomains, data: organizationDomains, @@ -181,7 +199,10 @@ export const useOrganizationEnterpriseConnection = (): UseOrganizationEnterprise prepareOwnershipVerification, attemptOwnershipVerification, revalidate: revalidateDomains, - } = __internal_useOrganizationDomains({ enrollmentMode: 'enterprise_sso' }); + } = __internal_useOrganizationDomains({ + enrollmentMode: 'enterprise_sso', + onOwnershipVerified: handleDomainOwnershipVerified, + }); const organizationDomainMutations = useMemo( () => ({ @@ -195,16 +216,8 @@ export const useOrganizationEnterpriseConnection = (): UseOrganizationEnterprise const enterpriseConnectionMutations = useMemo(() => { const createConnection: EnterpriseConnectionMutations['createConnection'] = provider => { - const primaryEmailAddress = user?.primaryEmailAddress; - const emailDomain = primaryEmailAddress?.emailAddress.split('@')[1]; - - // Connection name will always be defined due to the organization name - // Soon this logic will be moved to the Frontend API - const connectionName = emailDomain ?? organization?.name ?? ''; - return createEnterpriseConnection({ provider, - name: connectionName, domains: organizationDomains?.map(domain => domain.name), }); }; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfirmationStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfirmationStep.tsx index 292acd94583..b0518e00c3d 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfirmationStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfirmationStep.tsx @@ -216,26 +216,6 @@ const ConfigurationDetailsSection = (): JSX.Element => { {samlConnection?.idpEntityId} - - ({ gap: t.space.$3, paddingInlineStart: 0 })} - > - ({ width: t.space.$36, flexShrink: 0, whiteSpace: 'nowrap' })} - /> - - {samlConnection?.idpCertificate} - - diff --git a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx index 522ad8883f4..f530e4977e2 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx @@ -23,6 +23,7 @@ import { ClipboardInput } from '@/elements/ClipboardInput'; import { useCardState } from '@/elements/contexts'; import { Field } from '@/elements/FieldControl'; import { Form } from '@/elements/Form'; +import { Tooltip } from '@/elements/Tooltip'; import { Checkmark, Clipboard, Close } from '@/icons'; import { common } from '@/styledSystem'; import { useFormControl } from '@/ui/utils/useFormControl'; @@ -32,15 +33,20 @@ import { useConfigureSSO } from '../ConfigureSSOContext'; import { areAllOrganizationDomainsVerified } from '../domain/organizationEnterpriseConnection'; import { Step } from '../elements/Step'; import { useWizard } from '../elements/Wizard/WizardContext'; +import { RemoveDomainDialog } from '../RemoveDomainDialog'; export const OrganizationDomainsStep = (): JSX.Element => { const { t } = useLocalizations(); const { + enterpriseConnection, organizationDomains, + contentRef, organizationDomainMutations: { createDomain, revalidate }, + enterpriseConnectionMutations: { updateConnection }, } = useConfigureSSO(); const { goPrev, goNext, isFirstStep, isLastStep } = useWizard(); const card = useCardState(); + const [domainToRemove, setDomainToRemove] = useState(null); const handleCreateDomain = async (domain: string) => { card.setError(undefined); @@ -53,8 +59,27 @@ export const OrganizationDomainsStep = (): JSX.Element => { } }; + const handleRemoveDomain = async (domain: OrganizationDomainResource) => { + if (enterpriseConnection) { + const domains = enterpriseConnection.domains.filter(name => name !== domain.name); + await updateConnection(enterpriseConnection.id, { domains }); + } + + await domain.delete(); + await revalidate(); + }; + const hasAllDomainsVerified = areAllOrganizationDomainsVerified(organizationDomains); + // A connection needs at least one verified domain to point at, so the last + // remaining verified domain cannot be removed while a connection exists + const verifiedDomainCount = + organizationDomains?.filter(domain => domain.ownershipVerification?.status === 'verified').length ?? 0; + const lockLastVerifiedDomain = Boolean(enterpriseConnection); + const lastVerifiedDomainTooltip = enterpriseConnection?.active + ? localizationKeys('configureSSO.organizationDomainsStep.domainCard.removeButtonTooltip__lastVerifiedDomainActive') + : localizationKeys('configureSSO.organizationDomainsStep.domainCard.removeButtonTooltip__lastVerifiedDomain'); + return ( { ...common.unstyledScrollbar(t), })} > - {organizationDomains.map(domain => ( - { - // TODO ORGS-1623 - Add dialog for domain deletion confirmation - void domain.delete().then(() => revalidate()); - }} - /> - ))} + {organizationDomains.map(domain => { + const isVerified = domain.ownershipVerification?.status === 'verified'; + const isLastVerifiedDomain = isVerified && verifiedDomainCount === 1; + const isRemoveDisabled = lockLastVerifiedDomain && isLastVerifiedDomain; + return ( + setDomainToRemove(domain)} + isRemoveDisabled={isRemoveDisabled} + removeDisabledTooltip={lastVerifiedDomainTooltip} + /> + ); + })} )} @@ -128,6 +157,17 @@ export const OrganizationDomainsStep = (): JSX.Element => { /> + + {domainToRemove && ( + setDomainToRemove(null)} + domain={domainToRemove.name} + isConnectionActive={Boolean(enterpriseConnection?.active)} + onRemove={() => handleRemoveDomain(domainToRemove)} + contentRef={contentRef} + /> + )} ); }; @@ -291,9 +331,13 @@ const DomainSuggestion = ({ onSubmit }: { onSubmit: (domain: string) => Promise< const DomainCard = ({ domain, onRemove, + isRemoveDisabled = false, + removeDisabledTooltip, }: { domain: OrganizationDomainResource; onRemove: () => void; + isRemoveDisabled?: boolean; + removeDisabledTooltip?: ReturnType; }): JSX.Element | null => { if (!domain.name) { return null; @@ -303,6 +347,23 @@ const DomainCard = ({ const isVerified = ownershipVerification?.status === 'verified'; const cardId = isVerified ? 'verified' : 'unverified'; + const removeButton = ( + + ); + return ( - + {isRemoveDisabled && removeDisabledTooltip ? ( + + {removeButton} + + + ) : ( + removeButton + )} diff --git a/packages/ui/src/components/OrganizationProfile/SecuritySsoSection.tsx b/packages/ui/src/components/OrganizationProfile/SecuritySsoSection.tsx index fdd77fa47a9..b532e859123 100644 --- a/packages/ui/src/components/OrganizationProfile/SecuritySsoSection.tsx +++ b/packages/ui/src/components/OrganizationProfile/SecuritySsoSection.tsx @@ -141,7 +141,8 @@ const NotConfiguredContent = ({