diff --git a/.changeset/ripe-ways-greet.md b/.changeset/ripe-ways-greet.md new file mode 100644 index 00000000000..036c1af89b8 --- /dev/null +++ b/.changeset/ripe-ways-greet.md @@ -0,0 +1,8 @@ +--- +'@clerk/localizations': minor +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/ui': minor +--- + +Handle expired organization domains on self-serve SSO flow, allowing to trigger a new verification diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index f28044450e1..128731b292a 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -649,8 +649,12 @@ export const enUS: LocalizationResource = { }, organizationDomainsStep: { domainCard: { + badge__expired: 'Expired', badge__unverified: 'Unverified', badge__verified: 'Verified', + expiredAtLabel: + "Domain verification expired on {{ date | shortDate('en-US') }}. Verify again to generate a new DNS record.", + expiredLabel: 'Domain verification expired. Verify again to generate a new DNS record.', 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: { @@ -660,6 +664,7 @@ export const enUS: LocalizationResource = { valueLabel: 'Value', }, verifiedAtLabel: "Verified on {{ date | shortDate('en-US') }}", + verifyAgainButton: 'Verify again', }, domainSuggestion: { formButtonPrimary__add: 'Add {{domain}}', diff --git a/packages/shared/src/react/hooks/useOrganizationDomains.tsx b/packages/shared/src/react/hooks/useOrganizationDomains.tsx index 9965b31b6fd..427f90a9e8b 100644 --- a/packages/shared/src/react/hooks/useOrganizationDomains.tsx +++ b/packages/shared/src/react/hooks/useOrganizationDomains.tsx @@ -137,10 +137,7 @@ function useOrganizationDomains(params: UseOrganizationDomainsParams = {}): UseO const unverifiedOwnershipDomainIds = useMemo( () => (response?.data ?? []) - .filter( - (domain: OrganizationDomainResource) => - domain.ownershipVerification && domain.ownershipVerification.status !== 'verified', - ) + .filter((domain: OrganizationDomainResource) => domain.ownershipVerification?.status === 'unverified') .map((domain: OrganizationDomainResource) => domain.id), [response?.data], ); diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts index 765ca1d6208..24743561cde 100644 --- a/packages/shared/src/types/json.ts +++ b/packages/shared/src/types/json.ts @@ -20,6 +20,7 @@ import type { EmailAddressIdentifier, UsernameIdentifier } from './identifiers'; import type { ActClaim } from './jwtv2'; import type { OAuthProvider } from './oauth'; import type { + OrganizationDomainOwnershipVerificationStatus, OrganizationDomainOwnershipVerificationStrategy, OrganizationDomainVerificationStatus, OrganizationEnrollmentMode, @@ -436,7 +437,7 @@ export interface OrganizationDomainVerificationJSON { } export interface OrganizationDomainOwnershipVerificationJSON { - status: OrganizationDomainVerificationStatus; + status: OrganizationDomainOwnershipVerificationStatus; strategy: OrganizationDomainOwnershipVerificationStrategy; attempts: number | null; expire_at: number | null; diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 89859af9e7c..7e5910e24c4 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1390,7 +1390,11 @@ export type __internal_LocalizationResource = { domainCard: { badge__verified: LocalizationValue; badge__unverified: LocalizationValue; + badge__expired: LocalizationValue; verifiedAtLabel: LocalizationValue<'date'>; + expiredAtLabel: LocalizationValue<'date'>; + expiredLabel: LocalizationValue; + verifyAgainButton: LocalizationValue; removeButtonTooltip__lastVerifiedDomain: LocalizationValue; removeButtonTooltip__lastVerifiedDomainActive: LocalizationValue; txtRecord: { diff --git a/packages/shared/src/types/organizationDomain.ts b/packages/shared/src/types/organizationDomain.ts index 45b4d6d8d16..25e3b87a598 100644 --- a/packages/shared/src/types/organizationDomain.ts +++ b/packages/shared/src/types/organizationDomain.ts @@ -34,30 +34,51 @@ type OrganizationDomainVerificationStrategy = 'email_code'; /** * The current status of an Organization domain verification. * + * + * + * @inline + */ +export type OrganizationDomainVerificationStatus = 'unverified' | 'verified' | 'failed' | 'expired'; + +/** + * The current status of an Organization domain ownership verification. + * + * + * * @inline */ -export type OrganizationDomainVerificationStatus = 'unverified' | 'verified'; +export type OrganizationDomainOwnershipVerificationStatus = 'unverified' | 'verified' | 'expired'; /** * The strategy used to verify ownership of an Organization's domain. * @inline */ -export type OrganizationDomainOwnershipVerificationStrategy = 'txt' | 'legacy' | 'manual_override'; +export type OrganizationDomainOwnershipVerificationStrategy = 'txt' | 'legacy' | 'manual_override' | 'parent_domain'; /** * Holds the ownership verification details of an Organization's [Verified Domain](https://clerk.com/docs/guides/organizations/add-members/verified-domains). Ownership proves control of the underlying DNS domain, typically by publishing a TXT record, and is required before the domain can be used for enterprise SSO. */ export interface OrganizationDomainOwnershipVerification { /** - * The current status of the domain ownership verification. + * The current status of the domain ownership verification. A status of `expired` means the pending TXT challenge expired before ownership could be confirmed and a new challenge must be issued. */ - status: OrganizationDomainVerificationStatus; + status: OrganizationDomainOwnershipVerificationStatus; /** * The strategy used to verify ownership of the domain. * */ strategy: OrganizationDomainOwnershipVerificationStrategy; @@ -66,7 +87,7 @@ export interface OrganizationDomainOwnershipVerification { */ attempts: number | null; /** - * The date and time when the current ownership verification attempt expires, or `null` when there is no pending attempt. + * The date and time when the current ownership verification attempt expires, or `null` when there is no pending attempt. When `status` is `expired`, this is the date and time at which the attempt expired. */ expiresAt: Date | null; /** diff --git a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx index 60306b0b86c..8c449089080 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx @@ -25,7 +25,7 @@ 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 { Checkmark, Clipboard, Close, RotateLeftRight } from '@/icons'; import { common } from '@/styledSystem'; import { useFormControl } from '@/ui/utils/useFormControl'; import { getFieldError, getGlobalError } from '@/utils/errorHandler'; @@ -43,7 +43,7 @@ export const OrganizationDomainsStep = (): JSX.Element => { organizationDomains, organizationEnterpriseConnection, contentRef, - organizationDomainMutations: { createDomain, revalidate }, + organizationDomainMutations: { createDomain, revalidate, prepareOwnershipVerification }, enterpriseConnectionMutations: { updateConnection }, } = useConfigureSSO(); const { goPrev, goNext, isFirstStep, isLastStep } = useWizard(); @@ -81,6 +81,17 @@ export const OrganizationDomainsStep = (): JSX.Element => { } }; + const handlePrepareOwnershipVerification = async (domain: OrganizationDomainResource) => { + card.setError(undefined); + + try { + await prepareOwnershipVerification([domain]); + } catch (err: any) { + const apiError = getFieldError(err) ?? getGlobalError(err); + card.setError(apiError); + } + }; + const handleRemoveDomain = async (domain: OrganizationDomainResource) => { if (enterpriseConnection) { const domains = enterpriseConnection.domains.filter(name => name !== domain.name); @@ -158,6 +169,7 @@ export const OrganizationDomainsStep = (): JSX.Element => { key={domain.id} domain={domain} onRemove={() => setDomainToRemove(domain)} + onPrepareOwnershipVerification={() => handlePrepareOwnershipVerification(domain)} isRemoveDisabled={isRemoveDisabled} removeDisabledTooltip={lastVerifiedDomainTooltip} /> @@ -353,11 +365,13 @@ const DomainSuggestion = ({ onSubmit }: { onSubmit: (domain: string) => Promise< const DomainCard = ({ domain, onRemove, + onPrepareOwnershipVerification, isRemoveDisabled = false, removeDisabledTooltip, }: { domain: OrganizationDomainResource; onRemove: () => void; + onPrepareOwnershipVerification: () => Promise; isRemoveDisabled?: boolean; removeDisabledTooltip?: ReturnType; }): JSX.Element | null => { @@ -367,7 +381,8 @@ const DomainCard = ({ const ownershipVerification = domain.ownershipVerification; const isVerified = ownershipVerification?.status === 'verified'; - const cardId = isVerified ? 'verified' : 'unverified'; + const isExpired = ownershipVerification?.status === 'expired'; + const cardId = ownershipVerification?.status ?? 'unverified'; const removeButton = ( + + ); +}; + const TxtRecord = ({ ownershipVerification, }: { diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts index aa5237ecf5b..69eca6184fe 100644 --- a/packages/ui/src/customizables/elementDescriptors.ts +++ b/packages/ui/src/customizables/elementDescriptors.ts @@ -586,6 +586,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'configureSSOVerifyDomainCardRemoveButton', 'configureSSOVerifyDomainCardTxtRecord', 'configureSSOVerifyDomainCardTxtRecordValue', + 'configureSSOVerifyDomainCardExpired', 'configureSSOEmailVerificationForm', 'configureSSOEmailVerificationIcon', 'configureSSOEmailVerificationTitle', diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 1729a78636d..71d20488744 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -717,11 +717,12 @@ export type ElementsConfig = { configureSSOVerifyDomainErrorSubtitle: WithOptions; configureSSOVerifyDomainList: WithOptions; configureSSOVerifyDomainSuggestion: WithOptions; - configureSSOVerifyDomainCard: WithOptions<'verified' | 'unverified'>; - configureSSOVerifyDomainCardBadge: WithOptions<'verified' | 'unverified'>; + configureSSOVerifyDomainCard: WithOptions<'verified' | 'unverified' | 'expired'>; + configureSSOVerifyDomainCardBadge: WithOptions<'verified' | 'unverified' | 'expired'>; configureSSOVerifyDomainCardRemoveButton: WithOptions; configureSSOVerifyDomainCardTxtRecord: WithOptions; configureSSOVerifyDomainCardTxtRecordValue: WithOptions; + configureSSOVerifyDomainCardExpired: WithOptions; configureSSOEmailVerificationForm: WithOptions; configureSSOEmailVerificationIcon: WithOptions; configureSSOEmailVerificationTitle: WithOptions;