diff --git a/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx b/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx index 1a9e864b8..6ac58d30f 100644 --- a/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx +++ b/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx @@ -76,31 +76,29 @@ export const CompactLocationsPage = () => { }} > + -
- -
- {displayedLocations.map((location) => { - const isOpen = - location.id === openLocation || displayedLocations.length === 1; - return ( - { - if (isOpen) { - useAppStore.setState({ expandedLocation: null }); - } else { - useAppStore.setState({ expandedLocation: location.id }); - } - }} - /> - ); - })} -
+
+ {displayedLocations.map((location) => { + const isOpen = + location.id === openLocation || displayedLocations.length === 1; + return ( + { + if (isOpen) { + useAppStore.setState({ expandedLocation: null }); + } else { + useAppStore.setState({ expandedLocation: location.id }); + } + }} + /> + ); + })}
diff --git a/new-ui/src/pages/compact/CompactLocationsPage/components/InstanceSwitcher.tsx b/new-ui/src/pages/compact/CompactLocationsPage/components/InstanceSwitcher.tsx index c182bced3..ce3a9999e 100644 --- a/new-ui/src/pages/compact/CompactLocationsPage/components/InstanceSwitcher.tsx +++ b/new-ui/src/pages/compact/CompactLocationsPage/components/InstanceSwitcher.tsx @@ -1,4 +1,6 @@ import { useQuery } from '@tanstack/react-query'; +import { platform } from '@tauri-apps/plugin-os'; +import clsx from 'clsx'; import { useMemo } from 'react'; import { Select } from '../../../../shared/components/Select/Select'; import type { @@ -14,6 +16,8 @@ import { import type { OverviewViewSelection } from '../../../../shared/rust-api/types'; import { isPresent } from '../../../../shared/utils/isPresent'; +const isWindows = platform() === 'windows'; + export const InstanceSwitcher = () => { const { viewSelection: selectedInstance, setViewSelection } = useAppData(); @@ -76,12 +80,18 @@ export const InstanceSwitcher = () => { if (totalOptions <= 1) return null; return ( - { + setViewSelection(option.value); + }} + /> +
); }; diff --git a/new-ui/src/pages/compact/CompactLocationsPage/style.scss b/new-ui/src/pages/compact/CompactLocationsPage/style.scss index ede1152da..71ce18d87 100644 --- a/new-ui/src/pages/compact/CompactLocationsPage/style.scss +++ b/new-ui/src/pages/compact/CompactLocationsPage/style.scss @@ -3,20 +3,27 @@ flex-flow: column; height: 100dvh; - > .compact-footer { + & > .compact-footer { display: flex; flex-flow: column; } - > .window-header, - > .compact-footer { + & > .window-header, + & > .compact-footer { flex: 0 0 auto; } - .main-content { - display: flex; - flex-flow: column; - row-gap: var(--spacing-sm); + & > .instance-switcher { + flex: 0 0 auto; + margin-bottom: var(--spacing-sm); + box-sizing: border-box; + + // lines the select up with the cards inside .scroll-container.windows + &.windows { + scrollbar-gutter: stable; + overflow-y: hidden; + padding-right: var(--scroll-container-gutter); + } } .locations { diff --git a/new-ui/src/pages/full/ConfigureMfaPage/ConfigureMfaPage.tsx b/new-ui/src/pages/full/ConfigureMfaPage/ConfigureMfaPage.tsx index 5ac9b0dcb..697870a1a 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/ConfigureMfaPage.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/ConfigureMfaPage.tsx @@ -1,5 +1,6 @@ import { useNavigate } from '@tanstack/react-router'; import { useCallback } from 'react'; +import { isPresent } from '../../../shared/utils/isPresent'; import { ConfigureMfaTimeoutProvider, useConfigureMfaSessionExpired, @@ -18,16 +19,24 @@ export const ConfigureMfaPage = () => { const ConfigureMfaContent = () => { const navigate = useNavigate(); - const authorized = useConfigureMfaStore((s) => s.authorized); + // a late authorization on the selection screen waits for the picks + const inWizard = useConfigureMfaStore( + (s) => s.authorized && isPresent(s.selectedMethods), + ); const handleSessionExpired = useConfigureMfaSessionExpired(); const leave = useCallback(() => { navigate({ to: '/full/add' }); }, [navigate]); - return authorized ? ( - - ) : ( - + return ( + <> + {inWizard && ( + + )} + {!inWizard && ( + + )} + ); }; diff --git a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx b/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx index 3a640cdd2..1b44494a4 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx @@ -23,13 +23,19 @@ const factorText = (method: MfaMethodValue): FactorText => { description: `We'll send a temporary security code to your email. Enter the code to confirm it's you and continue.`, }; case MfaMethod.Oidc: - return { title: '', description: '' }; + return { + title: 'OpenID sign-in', + description: `Sign in with your identity provider in the browser to confirm it's you.`, + }; case MfaMethod.Biometric: return { title: '', description: '' }; case MfaMethod.MobileApprove: return { title: '', description: '' }; case MfaMethod.Fido2: - return { title: '', description: '' }; + return { + title: 'Security key', + description: `Use a security key registered to your account, such as a YubiKey, to confirm it's you.`, + }; } }; diff --git a/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.test.ts b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.test.ts new file mode 100644 index 000000000..9ec75d0fd --- /dev/null +++ b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { MfaMethod } from '../../../../shared/rust-api/types'; +import { ConfigureMfaStep } from '../types'; +import { applyAuthorization, useConfigureMfaStore } from './useConfigureMfaStore'; + +vi.mock('@tauri-apps/plugin-log', () => ({ error: vi.fn() })); +vi.mock('../../../../shared/rust-api/api', () => ({ api: {} })); + +const sessionId = 'session-1'; +const authorizeResult = { deadline_timestamp: 1_900_000_000, recovery_codes: [] }; + +describe('applyAuthorization', () => { + beforeEach(() => { + useConfigureMfaStore.getState().reset(); + useConfigureMfaStore.setState({ + sessionId, + configuredMethods: [MfaMethod.Email], + verificationMethods: [MfaMethod.Email], + }); + }); + + it('waits on the selection when the answer lands after Back', () => { + useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]); + useConfigureMfaStore.getState().backFromVerification(); + applyAuthorization(sessionId, authorizeResult); + + const state = useConfigureMfaStore.getState(); + expect(state.authorized).toBe(true); + expect(state.selectedMethods).toBeNull(); + expect(state.activeStep).toBe(ConfigureMfaStep.Configuration); + expect(state.deadline).not.toBeNull(); + }); + + it('sets up the picks confirmed after a late answer', () => { + useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]); + useConfigureMfaStore.getState().backFromVerification(); + applyAuthorization(sessionId, authorizeResult); + useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]); + + const state = useConfigureMfaStore.getState(); + expect(state.activeStep).toBe(ConfigureMfaStep.Fido2); + expect(state.deadline).not.toBeNull(); + }); + + it('finishes when the pick confirmed after a late answer is empty', () => { + useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]); + useConfigureMfaStore.getState().backFromVerification(); + applyAuthorization(sessionId, authorizeResult); + useConfigureMfaStore.getState().selectMethods([]); + + const state = useConfigureMfaStore.getState(); + expect(state.activeStep).toBe(ConfigureMfaStep.Finish); + expect(state.deadline).toBeNull(); + }); + + it('keeps the deadline of an unauthorized session with an empty pick', () => { + useConfigureMfaStore.setState({ deadline: '2030-01-01T00:00:00.000Z' }); + useConfigureMfaStore.getState().selectMethods([]); + + expect(useConfigureMfaStore.getState().deadline).toBe('2030-01-01T00:00:00.000Z'); + }); + + it('drops an answer for another session', () => { + useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]); + applyAuthorization('session-2', authorizeResult); + + expect(useConfigureMfaStore.getState().authorized).toBe(false); + }); +}); diff --git a/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx index ab4d7411f..a71e4b63e 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx @@ -25,6 +25,7 @@ import { isMfaFactorOfferable, isMfaSetupStep, mfaFactorStep, + verificationMethodsOf, } from '../utils'; type StoreValues = { @@ -41,7 +42,9 @@ type StoreValues = { selectedMethods: MfaMethodValue[] | null; /** Pre-ticked in the selection step, from the entry point or an earlier pass. */ initialSelection: MfaMethodValue[]; - /** Null until picked. Asked only when more than one code factor is configured. */ + /** most preferred first, from the session since an older Core rejects some factors */ + verificationMethods: MfaVerificationMethod[]; + /** null until picked, asked only when the session offers more than one method */ verificationMethod: MfaVerificationMethod | null; /** No factor was configured, so an emailed code was the only way in. */ emailFallback: boolean; @@ -103,6 +106,7 @@ const defaults: StoreValues = { completedMethods: [], selectedMethods: null, initialSelection: [], + verificationMethods: [], verificationMethod: null, emailFallback: false, deadline: null, @@ -125,11 +129,11 @@ interface Store extends StoreValues { selectVerificationMethod: (method: MfaVerificationMethod) => void; /** Keeps the current picks ticked and the session alive. */ backToSelection: () => void; + backFromVerification: () => void; /** The fresh deadline bounds every setup still to come, not just the next one. */ authorize: (response: MfaConfigAuthorizeResult) => void; factorConfigured: (method: MfaMethodValue, recoveryCodes: string[]) => void; next: () => void; - back: () => void; reset: () => void; } @@ -139,30 +143,41 @@ export const useConfigureMfaStore = create()( ...defaults, start: (instance, response, origin) => { // The fallback mails a code to the address on file, registering email along the way. - const codeFactors = response.email_fallback + const sessionMethods = response.email_fallback ? [MfaMethod.Email] : response.available_methods; + // the session and the snapshot may both list FIDO2 const configuredMethods = [ - ...codeFactors, - ...(instance.mfa_configured_methods ?? []).filter( - (method) => !isCodeMfaMethod(method), - ), + ...new Set([ + ...sessionMethods, + ...(instance.mfa_configured_methods ?? []).filter( + (method) => !isCodeMfaMethod(method), + ), + ]), ]; set({ ...defaults, instance, sessionId: response.session_id, configuredMethods, + verificationMethods: verificationMethodsOf(sessionMethods), emailFallback: response.email_fallback, deadline: dayjs.unix(response.deadline_timestamp).toISOString(), ...origin, }); }, selectMethods: (methods) => { - set((current) => ({ - selectedMethods: methods, - activeStep: firstStep({ ...current, selectedMethods: methods }), - })); + set((current) => { + const next = { ...current, selectedMethods: methods }; + return { + selectedMethods: methods, + activeStep: firstStep(next), + // picked after a late authorization, so the deadline was kept for it + ...(current.authorized && { + deadline: sessionDeadline(next, current.deadline), + }), + }; + }); }, selectVerificationMethod: (method) => { set({ verificationMethod: method }); @@ -175,17 +190,26 @@ export const useConfigureMfaStore = create()( activeStep: defaults.activeStep, })); }, + backFromVerification: () => { + if (isPresent(get().verificationMethod)) { + set({ verificationMethod: null }); + return; + } + get().backToSelection(); + }, authorize: (response) => { set((current) => { + const deadline = dayjs.unix(response.deadline_timestamp).toISOString(); + // a late answer can land after Back, the steps wait for its Continue + if (!isPresent(current.selectedMethods)) { + return { authorized: true, recoveryCodes: response.recovery_codes, deadline }; + } // The fallback enables email as it verifies, so only this authorization issues codes. const next = { ...current, recoveryCodes: response.recovery_codes }; return { authorized: true, recoveryCodes: response.recovery_codes, - deadline: sessionDeadline( - next, - dayjs.unix(response.deadline_timestamp).toISOString(), - ), + deadline: sessionDeadline(next, deadline), activeStep: firstStep(next), }; }); @@ -217,15 +241,6 @@ export const useConfigureMfaStore = create()( ); if (isPresent(next)) set({ activeStep: next }); }, - back: () => { - const current = get(); - const from = MFA_WIZARD_STEPS.indexOf(current.activeStep); - // A step with nothing left to do is not one to go back to. - const previous = remainingSteps(current) - .filter((step) => MFA_WIZARD_STEPS.indexOf(step) < from) - .at(-1); - if (isPresent(previous)) set({ activeStep: previous }); - }, reset: () => { set({ ...defaults }); }, @@ -234,7 +249,7 @@ export const useConfigureMfaStore = create()( name: 'configure-mfa-store', storage: createJSONStorage(() => sessionStorage), // Bumped on every shape change: a stored session is never resumable across one. - version: 11, + version: 12, }, ), ); @@ -263,6 +278,17 @@ export const startMfaConfiguration = async ( useConfigureMfaStore.setState({ initialSelection }); }; +/** applied even after the asking step unmounts, Core has authorized the session either way. + * a cancel resets sessionId, so a late answer for a discarded session is dropped */ +export const applyAuthorization = ( + sessionId: string, + result: MfaConfigAuthorizeResult, +): void => { + const store = useConfigureMfaStore.getState(); + if (store.sessionId !== sessionId) return; + store.authorize(result); +}; + /** A copy the proxy still holds expires on its own, so a failed cancel is not worth raising. */ export const discardMfaConfiguration = async (): Promise => { const { sessionId } = useConfigureMfaStore.getState(); diff --git a/new-ui/src/pages/full/ConfigureMfaPage/hooks/useMfaConfigErrorHandler.ts b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useMfaConfigErrorHandler.ts index 6fed37832..ad95430c9 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/hooks/useMfaConfigErrorHandler.ts +++ b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useMfaConfigErrorHandler.ts @@ -1,12 +1,17 @@ import { error as logError } from '@tauri-apps/plugin-log'; import { useCallback } from 'react'; import { + isMfaConfigAlreadyAuthorized, isMfaConfigCancelled, + isMfaConfigFailedPrecondition, + isMfaConfigForbidden, isMfaConfigInvalidCode, + isMfaConfigMethodNotConfigured, isMfaConfigNetworkError, isMfaConfigProxyError, isMfaConfigSecurityKeyError, isMfaConfigSessionExpired, + isMfaConfigTimeout, mfaErrorMessage, } from '../../../../shared/rust-api/mfaError'; import { showEdgeComsError } from '../components/EdgeComsError/useEdgeComsErrorStore'; @@ -41,11 +46,34 @@ export const useMfaConfigErrorHandler = ({ setError(hasCodeInput ? 'Invalid code' : mfaErrorMessage(err)); return; } + // an OpenID login as another user lands here too, Core ends the session for it if (isMfaConfigSessionExpired(err)) { setError('Configuration session expired, start again.'); onSessionExpired(); return; } + if (isMfaConfigAlreadyAuthorized(err)) { + setError('This session was already verified, start again.'); + onSessionExpired(); + return; + } + if (isMfaConfigMethodNotConfigured(err)) { + setError('This method is no longer set up for your account.'); + return; + } + if (isMfaConfigForbidden(err)) { + setError(mfaErrorMessage(err)); + return; + } + // a consumed or replaced challenge, the next attempt fetches a fresh one + if (isMfaConfigFailedPrecondition(err)) { + setError('Verification expired, try again.'); + return; + } + if (isMfaConfigTimeout(err)) { + setError('Sign-in timed out, try again.'); + return; + } // The backend writes these for the user (no key, wrong PIN, no touch), so show as is. if (isMfaConfigSecurityKeyError(err)) { setError(mfaErrorMessage(err)); diff --git a/new-ui/src/pages/full/ConfigureMfaPage/types.ts b/new-ui/src/pages/full/ConfigureMfaPage/types.ts index 4880a77f2..3be9ef2ba 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/types.ts +++ b/new-ui/src/pages/full/ConfigureMfaPage/types.ts @@ -26,7 +26,12 @@ export type MfaFactor = { repeatable: boolean; }; -/** Factors that can authorize a session, most preferred first. Core only accepts code factors. */ -export const MFA_VERIFICATION_METHODS = [MfaMethod.Totp, MfaMethod.Email] as const; +/** most preferred first */ +export const mfaVerificationMethods = [ + MfaMethod.Totp, + MfaMethod.Email, + MfaMethod.Fido2, + MfaMethod.Oidc, +] as const; -export type MfaVerificationMethod = (typeof MFA_VERIFICATION_METHODS)[number]; +export type MfaVerificationMethod = (typeof mfaVerificationMethods)[number]; diff --git a/new-ui/src/pages/full/ConfigureMfaPage/utils.ts b/new-ui/src/pages/full/ConfigureMfaPage/utils.ts index cb6cee57f..a2c3c2f8f 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/utils.ts +++ b/new-ui/src/pages/full/ConfigureMfaPage/utils.ts @@ -6,10 +6,10 @@ import { import { ConfigureMfaStep, type ConfigureMfaStepValue, - MFA_VERIFICATION_METHODS, MFA_WIZARD_STEPS, type MfaFactor, type MfaVerificationMethod, + mfaVerificationMethods, } from './types'; /** Keyed on the shared list, so a new factor fails to compile until mapped here. */ @@ -48,12 +48,15 @@ export const isMfaFactorOfferable = ( return factor.repeatable || !configuredMethods.includes(method); }; -/** A session reports only code factors, others come from the instance snapshot. */ +const codeMfaMethods: MfaMethodValue[] = [MfaMethod.Totp, MfaMethod.Email]; + +/** every Core reports code factors in a session but older ones leave out FIDO2, + * so the instance snapshot stays the source for the rest */ export const isCodeMfaMethod = (method: MfaMethodValue): boolean => - MFA_VERIFICATION_METHODS.some((code) => code === method); + codeMfaMethods.includes(method); -/** Most preferred first. */ +/** ordered by preference, not by the input */ export const verificationMethodsOf = ( - configuredMethods: MfaMethodValue[], + availableMethods: MfaMethodValue[], ): MfaVerificationMethod[] => - MFA_VERIFICATION_METHODS.filter((method) => configuredMethods.includes(method)); + mfaVerificationMethods.filter((method) => availableMethods.includes(method)); diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx index 8ddb79d79..3548ed070 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx @@ -1,12 +1,12 @@ -import { useMemo } from 'react'; import { MfaMethod } from '../../../../shared/rust-api/types'; import { isPresent } from '../../../../shared/utils/isPresent'; import { useConfigureMfaStore } from '../hooks/useConfigureMfaStore'; import type { MfaVerificationMethod } from '../types'; -import { verificationMethodsOf } from '../utils'; import { ConfigureSelectMethodsStep } from './ConfigureSelectMethodsStep/ConfigureSelectMethodsStep'; import { ConfigureSelectVerificationStep } from './ConfigureSelectVerificationStep/ConfigureSelectVerificationStep'; import { ConfigureVerifyEmailStep } from './ConfigureVerifyEmailStep/ConfigureVerifyEmailStep'; +import { ConfigureVerifyFido2Step } from './ConfigureVerifyFido2Step/ConfigureVerifyFido2Step'; +import { ConfigureVerifyOidcStep } from './ConfigureVerifyOidcStep/ConfigureVerifyOidcStep'; import { ConfigureVerifyTotpStep } from './ConfigureVerifyTotpStep/ConfigureVerifyTotpStep'; type Props = { @@ -16,15 +16,10 @@ type Props = { /** Picks what the wizard sets up, then verifies the session with an existing factor. */ export const ConfigureMfaVerify = ({ onCancel, onSessionExpired }: Props) => { - const configuredMethods = useConfigureMfaStore((s) => s.configuredMethods); + const candidates = useConfigureMfaStore((s) => s.verificationMethods); const methodsSelected = useConfigureMfaStore((s) => isPresent(s.selectedMethods)); const verificationMethod = useConfigureMfaStore((s) => s.verificationMethod); - const candidates = useMemo( - () => verificationMethodsOf(configuredMethods), - [configuredMethods], - ); - if (!methodsSelected) { return ; } @@ -33,23 +28,18 @@ export const ConfigureMfaVerify = ({ onCancel, onSessionExpired }: Props) => { return ; } + // for the type only, a session always offers a method or falls back to email const method: MfaVerificationMethod = verificationMethod ?? candidates[0] ?? MfaMethod.Email; switch (method) { case MfaMethod.Totp: - return ( - - ); + return ; case MfaMethod.Email: - return ( - - ); + return ; + case MfaMethod.Fido2: + return ; + case MfaMethod.Oidc: + return ; } }; diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx index 9d1834469..891111769 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { Button } from '../../../../../shared/components/Button/Button'; import { ButtonVariant } from '../../../../../shared/components/Button/types'; import { Controls } from '../../../../../shared/components/Controls/Controls'; @@ -7,18 +7,12 @@ import { FullPage } from '../../../../../shared/layouts/FullPage/FullPage'; import { ConfigureMfaVerificatorFactorSelector } from '../../components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector'; import { useConfigureMfaStore } from '../../hooks/useConfigureMfaStore'; import type { MfaVerificationMethod } from '../../types'; -import { verificationMethodsOf } from '../../utils'; import '../style.scss'; import './style.scss'; -/** Shown only when more than one code factor is configured. */ +/** shown only when the session offers more than one method */ export const ConfigureSelectVerificationStep = () => { - const configuredMethods = useConfigureMfaStore((s) => s.configuredMethods); - - const methods = useMemo( - () => verificationMethodsOf(configuredMethods), - [configuredMethods], - ); + const methods = useConfigureMfaStore((s) => s.verificationMethods); const [selected, setSelected] = useState(methods[0]); diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureVerifyEmailStep/ConfigureVerifyEmailStep.tsx b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureVerifyEmailStep/ConfigureVerifyEmailStep.tsx index a68937e71..6b4ae0d3b 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureVerifyEmailStep/ConfigureVerifyEmailStep.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureVerifyEmailStep/ConfigureVerifyEmailStep.tsx @@ -9,23 +9,19 @@ import { FullPage } from '../../../../../shared/layouts/FullPage/FullPage'; import { api } from '../../../../../shared/rust-api/api'; import { MfaMethod } from '../../../../../shared/rust-api/types'; import { isPresent } from '../../../../../shared/utils/isPresent'; -import { - discardMfaConfiguration, - useConfigureMfaStore, -} from '../../hooks/useConfigureMfaStore'; +import { useConfigureMfaStore } from '../../hooks/useConfigureMfaStore'; import { useMfaConfigErrorHandler } from '../../hooks/useMfaConfigErrorHandler'; import '../style.scss'; const CODE_LENGTH = 6; interface Props { - onCancel: () => void; /** The session outlived its deadline, so the whole flow has to start over. */ onSessionExpired: () => void; } /** The fallback when no factor exists, or email picked from the configured code factors. */ -export const ConfigureVerifyEmailStep = ({ onCancel, onSessionExpired }: Props) => { +export const ConfigureVerifyEmailStep = ({ onSessionExpired }: Props) => { const sessionId = useConfigureMfaStore((s) => s.sessionId); const [code, setCode] = useState(null); @@ -75,12 +71,7 @@ export const ConfigureVerifyEmailStep = ({ onCancel, onSessionExpired }: Props) onError: (err) => handleApiError(err), }); - const { mutate: cancel, isPending: isCancelling } = useMutation({ - mutationFn: discardMfaConfiguration, - onSettled: onCancel, - }); - - const isBusy = isRequestingCode || isSubmitting || isCancelling; + const isBusy = isRequestingCode || isSubmitting; const handleSubmit = useCallback( (pastedCode?: string) => { @@ -133,11 +124,11 @@ export const ConfigureVerifyEmailStep = ({ onCancel, onSessionExpired }: Props)