From ef148b6504e350307ca85dbdcd173846c148d961 Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Thu, 25 Jun 2026 02:05:20 -0600
Subject: [PATCH] fix(studio): org MFA enforcement toggle visibility and UX
(DEPR-606) (#47285)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## What kind of change does this PR introduce?
Bug fix. Resolves DEPR-606.
## What is the current behavior?
On org Security settings, the MFA enforcement switch could appear on
without a green track. Users without personal MFA saw a disabled toggle
with a tooltip.
## What is the new behavior?
- Switch checked state renders correctly (removed tooltip trigger from
the switch).
- Users who need personal MFA first see an admonition with a link to
account security instead of a disabled toggle.
I felt this was a better user experience and more straightforward than
the alternative: fighting the TooltipTrigger’s `data-state` conflict
with the Switch’s checked state.
| Before | After |
| --- | --- |
|
|
|
|
|
|
|
|
|
## Additional context
Test on `/org//security` in light mode with and without personal
MFA enabled. Or just hardcode the ternaries to see the various states on
local.
## Summary by CodeRabbit
* **New Features**
* Added clearer guidance for organization security settings when MFA
must first be enabled on a personal account.
* Improved loading behavior while member data is fetched.
* **Bug Fixes**
* Prevented the MFA enforcement form from showing until personal MFA
requirements are met.
* Refined the MFA toggle disabled logic to apply only when appropriate.
* **UI Improvements**
* Replaced the MFA tooltip with an in-page notice.
* Updated the primary action button label from “Save changes” to “Save.”
---------
Co-authored-by: Cursor
Co-authored-by: Joshen Lim
---
.../Organization/SecuritySettings.tsx | 107 +++++++++---------
1 file changed, 53 insertions(+), 54 deletions(-)
diff --git a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx
index a8ea11d9644..cba6de2cbbc 100644
--- a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx
+++ b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx
@@ -1,29 +1,18 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
+import Link from 'next/link'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
-import {
- Button,
- Card,
- CardContent,
- CardFooter,
- Form,
- FormControl,
- FormField,
- Switch,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from 'ui'
+import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui'
+import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { z } from 'zod'
import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
import AlertError from '@/components/ui/AlertError'
-import { InlineLink } from '@/components/ui/InlineLink'
import NoPermission from '@/components/ui/NoPermission'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
@@ -41,7 +30,13 @@ const schema = z.object({
export const SecuritySettings = () => {
const { slug } = useParams()
const { profile } = useProfile()
- const { data: members } = useOrganizationMembersQuery({ slug })
+ const {
+ data: members,
+ error: membersError,
+ isPending: isLoadingMembers,
+ isError: isMembersError,
+ isSuccess: isSuccessMembers,
+ } = useOrganizationMembersQuery({ slug })
const { can: canReadMfaConfig, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.READ,
@@ -89,7 +84,19 @@ export const SecuritySettings = () => {
}, [mfaConfig, form])
const hasMFAEnabled =
- members?.find((member) => member.primary_email == profile?.primary_email)?.mfa_enabled || false
+ members?.find((member) => member.primary_email == profile?.primary_email)?.mfa_enabled ?? false
+
+ const requiresPersonalMfa = isSuccessMembers && canUpdateMfaConfig && !hasMFAEnabled
+
+ const isLoadingMfaEnforcementSettings =
+ isLoadingMfa || isLoadingPermissions || isLoadingEntitlement || isLoadingMembers
+ const hasMfaConfigError = (isErrorMfa || Boolean(mfaError)) && hasAccessToEnforceMfa
+ const canShowMfaEnforcementForm =
+ isSuccessMfa && hasAccessToEnforceMfa && isSuccessMembers && !requiresPersonalMfa
+ const isMfaEnforcementSwitchDisabled =
+ !hasAccessToEnforceMfa || !canUpdateMfaConfig || isUpdatingMfa
+ const isSaveMfaEnforcementDisabled =
+ isMfaEnforcementSwitchDisabled || isLoadingMfa || !form.formState.isDirty
const onSubmit = (values: { enforceMfa: boolean }) => {
if (!slug || !hasAccessToEnforceMfa) return
@@ -108,7 +115,7 @@ export const SecuritySettings = () => {
/>
) : (
<>
- {isLoadingMfa || isLoadingPermissions || isLoadingEntitlement ? (
+ {isLoadingMfaEnforcementSettings ? (
@@ -116,13 +123,31 @@ export const SecuritySettings = () => {
) : !canReadMfaConfig ? (
- ) : null}
+ ) : (
+ requiresPersonalMfa && (
+
+ Set up MFA
+
+ }
+ />
+ )
+ )}
- {(isErrorMfa || mfaError) && hasAccessToEnforceMfa && (
+ {isMembersError && (
+
+ )}
+
+ {hasMfaConfigError && (
)}
- {isSuccessMfa && hasAccessToEnforceMfa && (
+ {canShowMfaEnforcementForm && (