From 82aaf9298c8e479e61a8889c90e90bb818123b18 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 29 May 2026 09:46:06 +0200 Subject: [PATCH 1/6] show loader when evaluating posture checks --- .../LocationCardMfaTotpView.tsx | 18 +++++++++++++++++- .../views/LocationCardMfaTotpView/style.scss | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss 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..7248c659 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx @@ -1,3 +1,4 @@ +import './style.scss'; import { useCallback, useEffect, useState } from 'react'; import { ThemeSpacing } from '../../../../types'; import { isPresent } from '../../../../utils/isPresent'; @@ -15,9 +16,10 @@ import { LocationViewHeader } from '../../components/LocationViewHeader/Location import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; +import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; export const LocationCardMfaTotpView = () => { - const { setView } = useLocationCardContext(); + const { setView, location } = useLocationCardContext(); const { verifyCode, isVerifying, verifyError, isStarting, startError } = useMfaConnect( MfaStartMethod.Totp, ); @@ -47,6 +49,20 @@ export const LocationCardMfaTotpView = () => { if (verifyError) setError(verifyError); }, [verifyError]); + const showLoader = + location.posture_check_required && isStarting && !startError; + if (showLoader) { + return ( +
+ +
+ +

Checking device requirements...

+
+
+ ); + } + return (
.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); + } + } +} From 113c17c62b91a75b6427efb74d81a060ee26bce4 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 29 May 2026 11:48:24 +0200 Subject: [PATCH 2/6] loader for email MFA screen --- .../LocationCardMfaEmailView.tsx | 17 ++++++++++++++++- .../views/LocationCardMfaEmailView/style.scss | 14 ++++++++++++++ .../LocationCardMfaTotpView.tsx | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/style.scss 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..38db6f1e 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -15,9 +15,10 @@ import { LocationViewHeader } from '../../components/LocationViewHeader/Location import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; +import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; export const LocationCardMfaEmailView = () => { - const { setView } = useLocationCardContext(); + const { setView, location } = useLocationCardContext(); const { verifyCode, isVerifying, verifyError, isStarting, startError } = useMfaConnect( MfaStartMethod.Email, ); @@ -47,6 +48,20 @@ 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 ( +
+ +
+ +

Checking device requirements...

+
+
+ ); + } return (
.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 7248c659..53ecc6d4 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx @@ -49,6 +49,7 @@ 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) { From af5edc7a16d0de06f691b39d774021e41f73ae4a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 29 May 2026 11:51:49 +0200 Subject: [PATCH 3/6] formatting --- .../LocationCardMfaEmailView/LocationCardMfaEmailView.tsx | 5 ++--- .../LocationCard/views/LocationCardMfaEmailView/style.scss | 1 + .../LocationCardMfaTotpView/LocationCardMfaTotpView.tsx | 5 ++--- .../LocationCard/views/LocationCardMfaTotpView/style.scss | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) 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 38db6f1e..d4763bf0 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -9,13 +9,13 @@ import { Divider } from '../../../Divider/Divider'; import { IconKind } from '../../../Icon'; import { IconButton } from '../../../IconButton/IconButton'; import { IconButtonVariant } from '../../../IconButton/types'; +import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; import { SizedBox } from '../../../SizedBox/SizedBox'; import { MfaStartMethod } from '../../api/startClientMfaSession'; import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; -import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; export const LocationCardMfaEmailView = () => { const { setView, location } = useLocationCardContext(); @@ -49,8 +49,7 @@ export const LocationCardMfaEmailView = () => { }, [verifyError]); // Show loader when posture is being evaluated - const showLoader = - location.posture_check_required && isStarting && !startError; + const showLoader = location.posture_check_required && isStarting && !startError; if (showLoader) { return (
diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/style.scss index 16d2c297..6dd24ee2 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/style.scss +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/style.scss @@ -6,6 +6,7 @@ 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 53ecc6d4..7a965048 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx @@ -10,13 +10,13 @@ import { Divider } from '../../../Divider/Divider'; import { IconKind } from '../../../Icon'; import { IconButton } from '../../../IconButton/IconButton'; import { IconButtonVariant } from '../../../IconButton/types'; +import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; import { SizedBox } from '../../../SizedBox/SizedBox'; import { MfaStartMethod } from '../../api/startClientMfaSession'; import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; -import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; export const LocationCardMfaTotpView = () => { const { setView, location } = useLocationCardContext(); @@ -50,8 +50,7 @@ export const LocationCardMfaTotpView = () => { }, [verifyError]); // Show loader when posture is being evaluated - const showLoader = - location.posture_check_required && isStarting && !startError; + const showLoader = location.posture_check_required && isStarting && !startError; if (showLoader) { return (
diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss index 16d2c297..6dd24ee2 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss @@ -6,6 +6,7 @@ align-items: center; justify-content: center; gap: var(--spacing-md); + p { font: var(--t-body-xs-500); color: var(--fg-white-70); From 1df79fc3fbc1191eaae1d56989dccecbf688491a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 29 May 2026 11:54:30 +0200 Subject: [PATCH 4/6] import styles --- .../views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx | 1 + 1 file changed, 1 insertion(+) 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 d4763bf0..b313039b 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -1,3 +1,4 @@ +import './style.scss'; import { useCallback, useEffect, useState } from 'react'; import { ThemeSpacing } from '../../../../types'; import { isPresent } from '../../../../utils/isPresent'; From 3b5d0416e776e2749743db99b34d376915f96f54 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 29 May 2026 12:03:35 +0200 Subject: [PATCH 5/6] loader component --- .../LocationCardMfaEmailView.tsx | 13 ++----------- .../LocationCardMfaStartLoader.tsx | 14 ++++++++++++++ .../style.scss | 0 .../LocationCardMfaTotpView.tsx | 13 ++----------- .../views/LocationCardMfaTotpView/style.scss | 15 --------------- 5 files changed, 18 insertions(+), 37 deletions(-) create mode 100644 new-ui/src/shared/components/LocationCard/views/LocationCardMfaStartLoader/LocationCardMfaStartLoader.tsx rename new-ui/src/shared/components/LocationCard/views/{LocationCardMfaEmailView => LocationCardMfaStartLoader}/style.scss (100%) delete mode 100644 new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss 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 b313039b..f25cca0b 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -1,4 +1,3 @@ -import './style.scss'; import { useCallback, useEffect, useState } from 'react'; import { ThemeSpacing } from '../../../../types'; import { isPresent } from '../../../../utils/isPresent'; @@ -10,13 +9,13 @@ import { Divider } from '../../../Divider/Divider'; import { IconKind } from '../../../Icon'; import { IconButton } from '../../../IconButton/IconButton'; import { IconButtonVariant } from '../../../IconButton/types'; -import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; import { SizedBox } from '../../../SizedBox/SizedBox'; import { MfaStartMethod } from '../../api/startClientMfaSession'; import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; +import { LocationCardMfaStartLoader } from '../LocationCardMfaStartLoader/LocationCardMfaStartLoader'; export const LocationCardMfaEmailView = () => { const { setView, location } = useLocationCardContext(); @@ -52,15 +51,7 @@ export const LocationCardMfaEmailView = () => { // Show loader when posture is being evaluated const showLoader = location.posture_check_required && isStarting && !startError; if (showLoader) { - return ( -
- -
- -

Checking device requirements...

-
-
- ); + return ; } return (
( +
+ +
+ +

Checking device requirements...

+
+
+); diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaStartLoader/style.scss similarity index 100% rename from new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/style.scss rename to new-ui/src/shared/components/LocationCard/views/LocationCardMfaStartLoader/style.scss 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 7a965048..402b67fb 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx @@ -1,4 +1,3 @@ -import './style.scss'; import { useCallback, useEffect, useState } from 'react'; import { ThemeSpacing } from '../../../../types'; import { isPresent } from '../../../../utils/isPresent'; @@ -10,13 +9,13 @@ import { Divider } from '../../../Divider/Divider'; import { IconKind } from '../../../Icon'; import { IconButton } from '../../../IconButton/IconButton'; import { IconButtonVariant } from '../../../IconButton/types'; -import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner'; import { SizedBox } from '../../../SizedBox/SizedBox'; import { MfaStartMethod } from '../../api/startClientMfaSession'; import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; import { useLocationCardContext } from '../../context/context'; import { LocationCardViews } from '../../context/types'; import { useMfaConnect } from '../../hooks/useMfaConnect'; +import { LocationCardMfaStartLoader } from '../LocationCardMfaStartLoader/LocationCardMfaStartLoader'; export const LocationCardMfaTotpView = () => { const { setView, location } = useLocationCardContext(); @@ -52,15 +51,7 @@ export const LocationCardMfaTotpView = () => { // Show loader when posture is being evaluated const showLoader = location.posture_check_required && isStarting && !startError; if (showLoader) { - return ( -
- -
- -

Checking device requirements...

-
-
- ); + return ; } return ( diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss deleted file mode 100644 index 6dd24ee2..00000000 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/style.scss +++ /dev/null @@ -1,15 +0,0 @@ -.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); - } - } -} From c5d1de7e5b6befa3f614977a2a093c924f76edfd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 29 May 2026 13:16:12 +0200 Subject: [PATCH 6/6] implement debounce on the loader --- .../LocationCard/hooks/useMfaConnect.ts | 22 +++++++++++++++++-- .../LocationCardMfaEmailView.tsx | 5 +++++ .../LocationCardMfaTotpView.tsx | 5 +++++ 3 files changed, 30 insertions(+), 2 deletions(-) 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 f25cca0b..bc9f938d 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -17,10 +17,15 @@ 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, 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); 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 402b67fb..d0eebdca 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx @@ -17,10 +17,15 @@ 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, 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);