Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/experience/src/Layout/SecondaryPageLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Props = {
readonly notification?: TFuncKey;
readonly onSkip?: () => void;
readonly isNavBarHidden?: boolean;
readonly isBackHidden?: boolean;
readonly children: React.ReactNode;
};

Expand All @@ -30,6 +31,7 @@ const SecondaryPageLayout = ({
notification,
onSkip,
isNavBarHidden,
isBackHidden,
children,
}: Props) => {
const { isMobile } = usePlatform();
Expand All @@ -40,6 +42,7 @@ const SecondaryPageLayout = ({
<PageMeta titleKey={title} />
<NavBar
isHidden={isNavBarHidden}
isBackHidden={isBackHidden}
onSkip={onSkip}
onBack={() => {
navigate(-1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const UserInteractionContextProvider = ({ children }: Props) => {
remove(StorageKeys.IdentifierInputValue);
remove(StorageKeys.ForgotPasswordIdentifierInputValue);
remove(StorageKeys.verificationIds);
remove(StorageKeys.OneTimeTokenSignIn);
Comment thread
wangsijie marked this conversation as resolved.
Outdated
}, [remove]);

const setVerificationId = useCallback((type: VerificationType, id: string) => {
Expand Down
7 changes: 6 additions & 1 deletion packages/experience/src/hooks/use-mfa-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { validate } from 'superstruct';

import useNavigateWithPreservedSearchParams from '@/hooks/use-navigate-with-preserved-search-params';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
import { UserMfaFlow } from '@/types';
import { type MfaFlowState, mfaErrorDataGuard } from '@/types/guard';
import { isNativeWebview } from '@/utils/native-sdk';
Expand All @@ -22,6 +23,7 @@ export type Options = {

const useMfaErrorHandler = ({ replace }: Options = {}) => {
const navigate = useNavigateWithPreservedSearchParams();
const { get } = useSessionStorage();
const { t } = useTranslation();
const { setToast } = useToast();
const startTotpBinding = useStartTotpBinding();
Expand Down Expand Up @@ -136,16 +138,19 @@ const useMfaErrorHandler = ({ replace }: Options = {}) => {
? factors.filter((factor) => factor !== MfaFactor.WebAuthn)
: factors;

const hideBack = Boolean(get(StorageKeys.OneTimeTokenSignIn));
Comment thread
wangsijie marked this conversation as resolved.
Outdated

await handleMfaRedirect(flow, {
availableFactors,
skippable,
maskedIdentifiers,
suggestion,
isWebAuthnUsedAsSignInPasskey,
hideBack,
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
});
};
},
[handleMfaRedirect, navigate, replace, setToast]
[get, handleMfaRedirect, navigate, replace, setToast]
);

const mfaVerificationErrorHandler = useMemo<ErrorHandlers>(
Expand Down
2 changes: 2 additions & 0 deletions packages/experience/src/hooks/use-session-storages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum StorageKeys {
IdentifierInputValue = 'identifier-input-value',
ForgotPasswordIdentifierInputValue = 'forgot-password-identifier-input-value',
verificationIds = 'verification-ids',
OneTimeTokenSignIn = 'one-time-token-sign-in',
}

const valueGuard = Object.freeze({
Expand All @@ -26,6 +27,7 @@ const valueGuard = Object.freeze({
[StorageKeys.IdentifierInputValue]: identifierInputValueGuard,
[StorageKeys.ForgotPasswordIdentifierInputValue]: identifierInputValueGuard,
[StorageKeys.verificationIds]: verificationIdsMapGuard,
[StorageKeys.OneTimeTokenSignIn]: s.literal(true),
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
wangsijie marked this conversation as resolved.
Outdated
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about the superstruct details
} satisfies { [key in StorageKeys]: s.Struct<any> });

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';

/**
* Hide the MFA setup back button when the user signed in via a one-time token.
* Going back would re-run token verification and fail with `one_time_token.token_consumed`.
*/
const useShouldHideMfaBackNavigation = () => {
const flowState = useMfaFlowState();
const { get } = useSessionStorage();

return Boolean(flowState?.hideBack || get(StorageKeys.OneTimeTokenSignIn));
Comment thread
wangsijie marked this conversation as resolved.
Outdated
};
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

export default useShouldHideMfaBackNavigation;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import Divider from '@/components/Divider';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useSkipOptionalMfa from '@/hooks/use-skip-optional-mfa';
import ErrorPage from '@/pages/ErrorPage';
Expand All @@ -26,6 +27,7 @@ const TotpBinding = () => {

const skipMfa = useSkipMfa();
const skipOptionalMfa = useSkipOptionalMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();

if (!totpBindingState || !verificationId) {
return <ErrorPage title="error.invalid_session" />;
Expand All @@ -35,6 +37,7 @@ const TotpBinding = () => {

return (
<SecondaryPageLayout
isBackHidden={shouldHideBack}
title="mfa.add_authenticator_app"
onSkip={conditional(skippable && (suggestion ? skipOptionalMfa : skipMfa))}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { sendVerificationCode } from '@/apis/experience';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useErrorHandler from '@/hooks/use-error-handler';
import useNavigateWithPreservedSearchParams from '@/hooks/use-navigate-with-preserved-search-params';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useSkipOptionalMfa from '@/hooks/use-skip-optional-mfa';
import IdentifierProfileForm from '@/pages/Continue/IdentifierProfileForm';
Expand Down Expand Up @@ -42,6 +43,7 @@ const VerificationCodeMfaBinding = ({

const skipMfa = useSkipMfa();
const skipOptionalMfa = useSkipOptionalMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();
const handleError = useErrorHandler();

const clearErrorMessage = useCallback(() => {
Expand Down Expand Up @@ -91,6 +93,7 @@ const VerificationCodeMfaBinding = ({

return (
<SecondaryPageLayout
isBackHidden={shouldHideBack}
title={titleKey}
description={descriptionKey}
onSkip={conditional(skippable && (suggestion ? skipOptionalMfa : skipMfa))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { validate } from 'superstruct';
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useWebAuthnOperation from '@/hooks/use-webauthn-operation';
import ErrorPage from '@/pages/ErrorPage';
Expand All @@ -25,6 +26,7 @@ const WebAuthnBinding = () => {

const handleWebAuthn = useWebAuthnOperation();
const skipMfa = useSkipMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();
const [isCreatingPasskey, setIsCreatingPasskey] = useState(false);

if (!webAuthnState || !verificationId) {
Expand All @@ -39,6 +41,7 @@ const WebAuthnBinding = () => {

return (
<SecondaryPageLayout
isBackHidden={shouldHideBack}
title="mfa.create_a_passkey"
description="mfa.create_passkey_description"
onSkip={conditional(skippable && skipMfa)}
Expand Down
3 changes: 3 additions & 0 deletions packages/experience/src/pages/MfaBinding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { conditional } from '@silverhand/essentials';
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import MfaFactorList from '@/containers/MfaFactorList';
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import useSkipOptionalMfa from '@/hooks/use-skip-optional-mfa';
import { UserMfaFlow } from '@/types';
Expand All @@ -11,6 +12,7 @@ import ErrorPage from '../ErrorPage';

const MfaBinding = () => {
const flowState = useMfaFlowState();
const shouldHideBack = useShouldHideMfaBackNavigation();
Comment thread
wangsijie marked this conversation as resolved.
Outdated
const skipMfa = useSkipMfa();
const skipOptionalMfa = useSkipOptionalMfa();

Expand All @@ -20,6 +22,7 @@ const MfaBinding = () => {

return (
<SecondaryPageLayout
isBackHidden={shouldHideBack}
title={flowState.suggestion ? 'mfa.add_another_mfa_factor' : 'mfa.add_mfa_factors'}
description={
flowState.suggestion ? 'mfa.add_another_mfa_description' : 'mfa.add_mfa_description'
Expand Down
3 changes: 3 additions & 0 deletions packages/experience/src/pages/MfaOnboarding/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import useEnableMfa from '@/hooks/use-enable-mfa';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useSkipMfa from '@/hooks/use-skip-mfa';
import Button from '@/shared/components/Button';

const MfaOnboarding = () => {
const skipMfa = useSkipMfa();
const enableMfa = useEnableMfa();
const shouldHideBack = useShouldHideMfaBackNavigation();

return (
<SecondaryPageLayout
isBackHidden={shouldHideBack}
title="mfa.onboarding"
description="mfa.onboarding_description"
onSkip={skipMfa}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { InputField } from '@/components/InputFields';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useSendMfaPayload from '@/hooks/use-send-mfa-payload';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import ErrorPage from '@/pages/ErrorPage';
import Button from '@/shared/components/Button';
import { UserMfaFlow } from '@/types';
Expand All @@ -21,6 +22,7 @@ type FormState = {

const BackupCodeVerification = () => {
const flowState = useMfaFlowState();
const shouldHideBack = useShouldHideMfaBackNavigation();
const sendMfaPayload = useSendMfaPayload();
const {
register,
Expand All @@ -45,7 +47,7 @@ const BackupCodeVerification = () => {
}

return (
<SecondaryPageLayout title="mfa.verify_mfa_factors">
<SecondaryPageLayout isBackHidden={shouldHideBack} title="mfa.verify_mfa_factors">
<SectionLayout
title="mfa.enter_a_backup_code"
description="mfa.enter_backup_code_description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UserInteractionContext from '@/Providers/UserInteractionContextProvider/U
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import MfaCodeVerification from '@/containers/MfaCodeVerification';
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import ErrorPage from '@/pages/ErrorPage';
import { UserMfaFlow } from '@/types';
import { codeVerificationTypeMap } from '@/utils/sign-in-experience';
Expand All @@ -15,6 +16,7 @@ import styles from './index.module.scss';

const EmailVerificationCode = () => {
const flowState = useMfaFlowState();
const shouldHideBack = useShouldHideMfaBackNavigation();
const { verificationIdsMap } = useContext(UserInteractionContext);

if (!flowState) {
Expand All @@ -30,7 +32,7 @@ const EmailVerificationCode = () => {
const maskedEmail = flowState.maskedIdentifiers?.[MfaFactor.EmailVerificationCode];

return (
<SecondaryPageLayout title="mfa.verify_mfa_factors">
<SecondaryPageLayout isBackHidden={shouldHideBack} title="mfa.verify_mfa_factors">
<SectionLayout
title="mfa.enter_email_verification_code"
description="mfa.enter_email_verification_code_description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UserInteractionContext from '@/Providers/UserInteractionContextProvider/U
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import MfaCodeVerification from '@/containers/MfaCodeVerification';
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import ErrorPage from '@/pages/ErrorPage';
import { UserMfaFlow } from '@/types';
import { codeVerificationTypeMap } from '@/utils/sign-in-experience';
Expand All @@ -15,6 +16,7 @@ import styles from './index.module.scss';

const PhoneVerificationCode = () => {
const flowState = useMfaFlowState();
const shouldHideBack = useShouldHideMfaBackNavigation();
const { verificationIdsMap } = useContext(UserInteractionContext);

if (!flowState) {
Expand All @@ -30,7 +32,7 @@ const PhoneVerificationCode = () => {
const maskedPhone = flowState.maskedIdentifiers?.[MfaFactor.PhoneVerificationCode];

return (
<SecondaryPageLayout title="mfa.verify_mfa_factors">
<SecondaryPageLayout isBackHidden={shouldHideBack} title="mfa.verify_mfa_factors">
<SectionLayout
title="mfa.enter_phone_verification_code"
description="mfa.enter_phone_verification_code_description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import SectionLayout from '@/Layout/SectionLayout';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import TotpCodeVerification from '@/containers/TotpCodeVerification';
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import ErrorPage from '@/pages/ErrorPage';
import { UserMfaFlow } from '@/types';

import styles from './index.module.scss';

const TotpVerification = () => {
const flowState = useMfaFlowState();
const shouldHideBack = useShouldHideMfaBackNavigation();

if (!flowState) {
return <ErrorPage title="error.invalid_session" />;
}

return (
<SecondaryPageLayout title="mfa.verify_mfa_factors">
<SecondaryPageLayout isBackHidden={shouldHideBack} title="mfa.verify_mfa_factors">
<SectionLayout
title="mfa.enter_one_time_code"
description="mfa.enter_one_time_code_description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import SectionLayout from '@/Layout/SectionLayout';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import SwitchMfaFactorsLink from '@/components/SwitchMfaFactorsLink';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import useWebAuthnOperation from '@/hooks/use-webauthn-operation';
import ErrorPage from '@/pages/ErrorPage';
import Button from '@/shared/components/Button';
Expand All @@ -23,6 +24,7 @@ const WebAuthnVerification = () => {
const verificationId = verificationIdsMap[VerificationType.WebAuthn];

const handleWebAuthn = useWebAuthnOperation();
const shouldHideBack = useShouldHideMfaBackNavigation();
const [isVerifying, setIsVerifying] = useState(false);

if (!webAuthnState || !verificationId) {
Expand All @@ -36,7 +38,7 @@ const WebAuthnVerification = () => {
}

return (
<SecondaryPageLayout title="mfa.verify_mfa_factors">
<SecondaryPageLayout isBackHidden={shouldHideBack} title="mfa.verify_mfa_factors">
<SectionLayout
title="mfa.verify_via_passkey"
description="mfa.verify_via_passkey_description"
Expand Down
8 changes: 7 additions & 1 deletion packages/experience/src/pages/MfaVerification/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
import MfaFactorList from '@/containers/MfaFactorList';
import useMfaFlowState from '@/hooks/use-mfa-factors-state';
import useShouldHideMfaBackNavigation from '@/hooks/use-should-hide-mfa-back-navigation';
import { UserMfaFlow } from '@/types';

import ErrorPage from '../ErrorPage';

const MfaVerification = () => {
const flowState = useMfaFlowState();
const shouldHideBack = useShouldHideMfaBackNavigation();

if (!flowState) {
return <ErrorPage title="error.invalid_session" />;
}

return (
<SecondaryPageLayout title="mfa.verify_mfa_factors" description="mfa.verify_mfa_description">
<SecondaryPageLayout
isBackHidden={shouldHideBack}
title="mfa.verify_mfa_factors"
description="mfa.verify_mfa_description"
>
<MfaFactorList flow={UserMfaFlow.MfaVerification} flowState={flowState} />
</SecondaryPageLayout>
);
Expand Down
5 changes: 5 additions & 0 deletions packages/experience/src/pages/OneTimeToken/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
signInWithOneTimeToken,
} from '@/apis/experience';
import useApi from '@/hooks/use-api';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
Comment thread
wangsijie marked this conversation as resolved.
Outdated
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import useNavigateWithPreservedSearchParams from '@/hooks/use-navigate-with-preserved-search-params';
Expand All @@ -30,6 +31,7 @@ const OneTimeToken = () => {
const hasTermsAgreed = useRef(false);
const isSubmitted = useRef(false);

const { set: setSessionStorage } = useSessionStorage();
const asyncIdentifyUserAndSubmit = useApi(identifyAndSubmitInteraction);
const asyncSignInWithOneTimeToken = useApi(signInWithOneTimeToken);
const asyncRegisterWithVerifiedIdentifier = useApi(registerWithVerifiedIdentifier);
Expand Down Expand Up @@ -184,6 +186,8 @@ const OneTimeToken = () => {
return;
}

setSessionStorage(StorageKeys.OneTimeTokenSignIn, true);
Comment thread
wangsijie marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
wangsijie marked this conversation as resolved.
Outdated

// Set email identifier to the <HiddenIdentifierInput />, so that when being asked for fulfilling
// the password later, the browser password manager can pick up both the email and the password.
setIdentifierInputValue({ type: SignInIdentifier.Email, value: email });
Expand All @@ -198,6 +202,7 @@ const OneTimeToken = () => {
handleError,
navigate,
setIdentifierInputValue,
setSessionStorage,
submit,
termsValidation,
]);
Expand Down
Loading
Loading