From 41bb3a62da8fbb1b4e8ac3dd439d32b4d4af18a7 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Tue, 30 Jun 2026 14:38:12 +0200 Subject: [PATCH] feat: rework SSO settings into Identity & Access setup flow Rename the page and sidebar entry to "Identity & Access". Auto-refresh domain/connection state every 5s while setup is incomplete. Lead the first run with domain verification, explain the admin-portal link by intent in the popup, and split the pre-active empty state into Domains and SSO sections (SSO appears once a domain is verified). Unify the portal-opening buttons on a single external-link style. --- .../OrganizationSettingsSideMenu.tsx | 2 +- .../route.tsx | 176 +++++++++++------- 2 files changed, 111 insertions(+), 67 deletions(-) diff --git a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx index cd7cc4a8e09..ada3aac5e5a 100644 --- a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx +++ b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx @@ -149,7 +149,7 @@ export function OrganizationSettingsSideMenu({ )} {isSsoUsingPlugin && ( [{ title: "SSO settings | Trigger.dev" }]; +export const meta: MetaFunction = () => [{ title: "Identity & Access | Trigger.dev" }]; const Params = z.object({ organizationSlug: z.string() }); @@ -286,10 +286,12 @@ export default function Page() { draftJitRoleId !== initialJitRoleId; const [portalUrl, setPortalUrl] = useState(null); + const [portalIntent, setPortalIntent] = useState<"sso" | "domain_verification" | null>(null); const [enforceModalOpen, setEnforceModalOpen] = useState(false); const portalFetcher = useFetcher<{ ok: boolean; url?: string; error?: string }>(); const saveFetcher = useFetcher(); const isSaving = saveFetcher.state !== "idle"; + const revalidator = useRevalidator(); useEffect(() => { if (portalFetcher.data?.ok && portalFetcher.data.url) { @@ -297,8 +299,33 @@ export default function Page() { } }, [portalFetcher.data]); + // Poll for fresh domain/connection state only while setup is incomplete: + // the user is finishing steps in the admin portal (another tab) and we + // want the page to reflect them without a manual reload. This covers both + // pre-active states (no IdP org yet, and IdP org but no active connection). + // Once there's an active connection we stop — the ActiveConnectionState form + // holds local draft edits that a revalidation must not stomp. The upsell + // state is excluded by `isEntitled`. + const shouldPoll = isEntitled && !hasActive; + useEffect(() => { + if (!shouldPoll) return; + const id = setInterval(() => { + if ( + revalidator.state !== "idle" || + portalFetcher.state !== "idle" || + saveFetcher.state !== "idle" || + (typeof document !== "undefined" && document.visibilityState === "hidden") + ) { + return; + } + revalidator.revalidate(); + }, 5000); + return () => clearInterval(id); + }, [shouldPoll, revalidator, portalFetcher.state, saveFetcher.state]); + const openPortal = (intent: "sso" | "domain_verification") => { setPortalUrl(null); + setPortalIntent(intent); portalFetcher.submit({ action: "portal_link", intent }, { method: "POST" }); }; @@ -317,14 +344,14 @@ export default function Page() { return ( - + {!isEntitled ? ( ) : !status.hasIdpOrg ? ( - openPortal("sso")} /> + openPortal("domain_verification")} /> ) : !hasActive ? ( openPortal("sso")} + onOpenPortal={openPortal} onToggleEnforced={(next) => { // Going on→off is harmless; going off→on locks users out so // we still require explicit confirmation. The modal updates @@ -361,7 +388,7 @@ export default function Page() { - setPortalUrl(null)} /> + setPortalUrl(null)} /> void }) { Configure SSO for your organization Single sign-on lets your IT admins manage who can access Trigger.dev through your identity - provider (Okta, Azure AD, Google Workspace, OneLogin, and more). The first click opens the - admin portal in a 5-minute single-use link. + provider (Okta, Azure AD, Google Workspace, OneLogin, and more). - ); @@ -439,51 +469,49 @@ function NoActiveConnectionState({ }) { const verifiedDomains = domains.filter((d) => d.state === "verified"); const failedDomains = domains.filter((d) => d.state === "failed"); - const pendingDomains = domains.filter((d) => d.state === "pending"); - const hasUnresolved = failedDomains.length > 0 || pendingDomains.length > 0; + const hasVerifiedDomain = verifiedDomains.length > 0; return ( -
- {failedDomains.length > 0 && ( - - {failedDomains.length === 1 - ? `Domain verification failed for ${failedDomains[0].domain}. Re-check the DNS records in the admin portal and re-run verification.` - : `${failedDomains.length} domains failed verification. Re-check the DNS records in the admin portal and re-run verification.`} - - )} - {failedDomains.length === 0 && verifiedDomains.length > 0 && ( - - {verifiedDomains.length === 1 - ? `Domain verified: ${verifiedDomains[0].domain}. Continue in the admin portal to finish setting up your identity provider connection.` - : `${verifiedDomains.length} domains verified. Continue in the admin portal to finish setting up your identity provider connection.`} - - )} - {failedDomains.length === 0 && verifiedDomains.length === 0 && ( - - Not yet configured. Continue in the admin portal to verify a domain and set up your - identity provider connection. - - )} +
+
+ Domains + + Verify the email domains your team signs in with. Once a domain is verified you can + connect your identity provider. + + {failedDomains.length > 0 && ( + + {failedDomains.length === 1 + ? `Domain verification failed for ${failedDomains[0].domain}. Re-check the DNS records in the admin portal and re-run verification.` + : `${failedDomains.length} domains failed verification. Re-check the DNS records in the admin portal and re-run verification.`} + + )} + {domains.length > 0 && } + +
- {domains.length > 0 && ( + {hasVerifiedDomain && (
- Domains - + SSO + + Connect your identity provider to finish setting up single sign-on for your verified + domains. + +
)} - -
- - -
); } @@ -551,7 +579,7 @@ function ActiveConnectionState({ draftJitRoleId, isDirty, isSaving, - onTogglePortal, + onOpenPortal, onToggleEnforced, onToggleJit, onChangeJitRole, @@ -571,7 +599,7 @@ function ActiveConnectionState({ draftJitRoleId: string; isDirty: boolean; isSaving: boolean; - onTogglePortal: () => void; + onOpenPortal: (intent: "sso" | "domain_verification") => void; onToggleEnforced: (next: boolean) => void; onToggleJit: (next: boolean) => void; onChangeJitRole: (roleId: string | null) => void; @@ -594,6 +622,13 @@ function ActiveConnectionState({
))} +
@@ -605,6 +640,13 @@ function ActiveConnectionState({ ) : ( )} +
@@ -667,18 +709,7 @@ function ActiveConnectionState({ }
-
- { - e.preventDefault(); - onTogglePortal(); - }} - > - Open admin portal - +
@@ -688,14 +719,27 @@ function ActiveConnectionState({ ); } -function PortalLinkDialog({ url, onClose }: { url: string | null; onClose: () => void }) { +function PortalLinkDialog({ + url, + intent, + onClose, +}: { + url: string | null; + intent: "sso" | "domain_verification" | null; + onClose: () => void; +}) { + const purpose = + intent === "domain_verification" + ? "This single-use link opens domain verification. Send it to whoever manages your DNS or identity provider so they can confirm your organization owns its email domains." + : intent === "sso" + ? "This single-use link opens identity-provider setup. Send it to whoever manages your identity provider so they can connect it to Trigger.dev." + : "This single-use link opens your organization's SSO setup."; return ( (open ? undefined : onClose())}> Admin portal link - This link is active for 5 minutes — copy it and share it with your IT contact via whatever - channel you prefer. + {purpose} The link expires 5 minutes after you open this dialog.
{url ?? ""}