diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts index 67c52e72..fabfb5af 100644 --- a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts @@ -24,11 +24,25 @@ type MfaErrorResponse = { type CodeMfaStartMethod = Extract; -export const useMfaConnect = (method: CodeMfaStartMethod) => { +type UseMfaConnectOptions = { + debounceMs?: number; +}; + +const waitForMinimumDuration = async (startedAt: number, minimumMs: number) => { + const remainingMs = Math.max(minimumMs - (performance.now() - startedAt), 0); + if (remainingMs === 0) return; + + await new Promise((resolve) => window.setTimeout(resolve, remainingMs)); +}; + +export const useMfaConnect = ( + method: CodeMfaStartMethod, + { debounceMs = 0 }: UseMfaConnectOptions = {}, +) => { const { location, setPostureError, setView } = useLocationCardContext(); const [token, setToken] = useState(null); - const [isStarting, setIsStarting] = useState(false); + const [isStarting, setIsStarting] = useState(debounceMs > 0); const [startError, setStartError] = useState(null); const [isVerifying, setIsVerifying] = useState(false); const [verifyError, setVerifyError] = useState(null); @@ -55,7 +69,9 @@ export const useMfaConnect = (method: CodeMfaStartMethod) => { // biome-ignore lint/correctness/useExhaustiveDependencies: intentional one-shot trigger via startCalled ref useEffect(() => { if (!instance || startCalled.current) return; + startCalled.current = true; + const startedAt = performance.now(); setIsStarting(true); @@ -66,9 +82,11 @@ export const useMfaConnect = (method: CodeMfaStartMethod) => { location, method, }); + await waitForMinimumDuration(startedAt, debounceMs); setRequestHeaders(headers); setToken(response.token); } catch (err) { + await waitForMinimumDuration(startedAt, debounceMs); if (handleMfaStartError({ err, location, setPostureError, setView })) { return; } diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx index 633578d8..bc9f938d 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -15,11 +15,17 @@ import { LocationViewHeader } from '../../components/LocationViewHeader/Location import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; +import { LocationCardMfaStartLoader } from '../LocationCardMfaStartLoader/LocationCardMfaStartLoader'; + +const MIN_POSTURE_LOADER_MS = 500; export const LocationCardMfaEmailView = () => { - const { setView } = useLocationCardContext(); + const { setView, location } = useLocationCardContext(); const { verifyCode, isVerifying, verifyError, isStarting, startError } = useMfaConnect( MfaStartMethod.Email, + { + debounceMs: location.posture_check_required ? MIN_POSTURE_LOADER_MS : 0, + }, ); const [emailCode, setEmailCode] = useState(null); @@ -47,6 +53,11 @@ export const LocationCardMfaEmailView = () => { if (verifyError) setError(verifyError); }, [verifyError]); + // Show loader when posture is being evaluated + const showLoader = location.posture_check_required && isStarting && !startError; + if (showLoader) { + return ; + } return (
( +
+ +
+ +

Checking device requirements...

+
+
+); diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaStartLoader/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaStartLoader/style.scss new file mode 100644 index 00000000..6dd24ee2 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaStartLoader/style.scss @@ -0,0 +1,15 @@ +.mfa-start-loader { + > .loader-content { + display: flex; + min-height: 140px; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-md); + + p { + font: var(--t-body-xs-500); + color: var(--fg-white-70); + } + } +} diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx index 4bb2ff29..d0eebdca 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx @@ -15,11 +15,17 @@ import { LocationViewHeader } from '../../components/LocationViewHeader/Location import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; +import { LocationCardMfaStartLoader } from '../LocationCardMfaStartLoader/LocationCardMfaStartLoader'; + +const MIN_POSTURE_LOADER_MS = 500; export const LocationCardMfaTotpView = () => { - const { setView } = useLocationCardContext(); + const { setView, location } = useLocationCardContext(); const { verifyCode, isVerifying, verifyError, isStarting, startError } = useMfaConnect( MfaStartMethod.Totp, + { + debounceMs: location.posture_check_required ? MIN_POSTURE_LOADER_MS : 0, + }, ); const [totpCode, setTotpCode] = useState(null); @@ -47,6 +53,12 @@ export const LocationCardMfaTotpView = () => { if (verifyError) setError(verifyError); }, [verifyError]); + // Show loader when posture is being evaluated + const showLoader = location.posture_check_required && isStarting && !startError; + if (showLoader) { + return ; + } + return (