diff --git a/.changeset/mfa-totp-verify-back-button.md b/.changeset/mfa-totp-verify-back-button.md new file mode 100644 index 00000000000..ffbe0627b1c --- /dev/null +++ b/.changeset/mfa-totp-verify-back-button.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Add a "Back" action to the authenticator-app verification step in ``, so a user who needs to re-scan can return to the QR code instead of cancelling the whole setup. Going back now reuses the TOTP secret already issued rather than generating a new one, keeping any code the user has already scanned valid. diff --git a/packages/ui/src/components/UserProfile/AddAuthenticatorApp.tsx b/packages/ui/src/components/UserProfile/AddAuthenticatorApp.tsx index e41ae5f6567..7b4c90149ce 100644 --- a/packages/ui/src/components/UserProfile/AddAuthenticatorApp.tsx +++ b/packages/ui/src/components/UserProfile/AddAuthenticatorApp.tsx @@ -18,17 +18,18 @@ import { useActionContext } from '../../elements/Action/ActionRoot'; type AddAuthenticatorAppProps = FormProps & { title: LocalizationKey; + pendingTotpRef: React.MutableRefObject; }; type DisplayFormat = 'qr' | 'uri'; export const AddAuthenticatorApp = withCardStateProvider((props: AddAuthenticatorAppProps) => { - const { title, onSuccess, onReset } = props; + const { title, onSuccess, onReset, pendingTotpRef } = props; const { user } = useUser(); const card = useCardState(); const createTOTP = useReverification(() => user?.createTOTP()); const { close } = useActionContext(); - const [totp, setTOTP] = React.useState(undefined); + const [totp, setTOTP] = React.useState(pendingTotpRef.current); const [displayFormat, setDisplayFormat] = React.useState('qr'); // TODO: React18 @@ -38,8 +39,18 @@ export const AddAuthenticatorApp = withCardStateProvider((props: AddAuthenticato return; } + // Each createTOTP() mints a new secret server-side, so reuse the one already + // issued this session — otherwise navigating back here invalidates the QR the + // user has scanned. + if (pendingTotpRef.current) { + return; + } + void createTOTP() - .then(totp => setTOTP(totp)) + .then(totp => { + pendingTotpRef.current = totp; + setTOTP(totp); + }) .catch(err => { if (isClerkRuntimeError(err) && err.code === 'reverification_cancelled') { return close(); diff --git a/packages/ui/src/components/UserProfile/MfaTOTPScreen.tsx b/packages/ui/src/components/UserProfile/MfaTOTPScreen.tsx index 6793997b06c..9d51e6546f9 100644 --- a/packages/ui/src/components/UserProfile/MfaTOTPScreen.tsx +++ b/packages/ui/src/components/UserProfile/MfaTOTPScreen.tsx @@ -15,7 +15,8 @@ type MfaTOTPFormProps = FormProps; export const MfaTOTPScreen = withCardStateProvider((props: MfaTOTPFormProps) => { const { onReset } = props; const wizard = useWizard(); - const ref = React.useRef(); + const pendingTotpRef = React.useRef(); + const verifiedTotpRef = React.useRef(); return ( @@ -23,12 +24,14 @@ export const MfaTOTPScreen = withCardStateProvider((props: MfaTOTPFormProps) => title={localizationKeys('userProfile.mfaTOTPPage.title')} onSuccess={wizard.nextStep} onReset={onReset} + pendingTotpRef={pendingTotpRef} /> contents={ } /> diff --git a/packages/ui/src/components/UserProfile/VerifyTOTP.tsx b/packages/ui/src/components/UserProfile/VerifyTOTP.tsx index 7aad031dccf..3fe0dda0464 100644 --- a/packages/ui/src/components/UserProfile/VerifyTOTP.tsx +++ b/packages/ui/src/components/UserProfile/VerifyTOTP.tsx @@ -12,11 +12,12 @@ import { FormContainer } from '@/ui/elements/FormContainer'; import { Button, Col, descriptors, localizationKeys } from '../../customizables'; type VerifyTOTPProps = FormProps & { - resourceRef: React.MutableRefObject; + verifiedTotpRef: React.MutableRefObject; + onBack: () => void; }; export const VerifyTOTP = withCardStateProvider((props: VerifyTOTPProps) => { - const { onSuccess, onReset, resourceRef } = props; + const { onSuccess, onReset, onBack, verifiedTotpRef } = props; const { user } = useUser(); const otp = useFieldOTP({ @@ -27,7 +28,7 @@ export const VerifyTOTP = withCardStateProvider((props: VerifyTOTPProps) => { .catch(reject); }, onResolve: a => { - resourceRef.current = a; + verifiedTotpRef.current = a; onSuccess(); }, }); @@ -50,6 +51,14 @@ export const VerifyTOTP = withCardStateProvider((props: VerifyTOTPProps) => { localizationKey={localizationKeys('userProfile.formButtonReset')} elementDescriptor={descriptors.formButtonReset} /> + +