import { zodResolver } from '@hookform/resolvers/zod' import { useQueryClient } from '@tanstack/react-query' import { LOCAL_STORAGE_KEYS } from 'common' import { useEffect, useState } from 'react' import { useForm, type SubmitHandler } from 'react-hook-form' import { toast } from 'sonner' import { Form, FormControl, FormField, Input } from 'ui' import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { z } from 'zod' import InformationBox from '@/components/ui/InformationBox' import { organizationKeys } from '@/data/organizations/keys' import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation' import { useMfaEnrollMutation } from '@/data/profile/mfa-enroll-mutation' import { useMfaUnenrollMutation } from '@/data/profile/mfa-unenroll-mutation' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' type TOTP = { qr_code: string; secret: string; uri: string } interface AddNewFactorModalProps { visible: boolean onClose: () => void } export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => { const { data, mutate: enroll, isPending: isEnrolling, reset } = useMfaEnrollMutation() useEffect(() => { if (!visible) reset() }, [reset, visible]) return ( <> } isLoading={isEnrolling} onClose={onClose} /> ) } interface FirstStepProps { visible: boolean isEnrolling: boolean reset: () => void enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void onClose: () => void } const FirstStep = ({ visible, isEnrolling, enroll, onClose }: FirstStepProps) => { const FormSchema = z.object({ name: z.string().min(1, 'Please provide a name to identify this app'), }) const form = useForm>({ resolver: zodResolver(FormSchema as any), defaultValues: { name: '' }, mode: 'onChange', }) const onSubmit: SubmitHandler> = async (values) => { enroll({ factorType: 'totp', friendlyName: values.name }) } useEffect(() => { if (!visible) { // Generate a name with a number between 0 and 1000 form.reset({ name: `App ${Math.floor(Math.random() * 1000)}` }) } }, [form, visible]) return (
( )} />
) } interface SecondStepProps { visible: boolean factorName: string factor?: { id: string type: 'totp' totp: TOTP } isLoading: boolean onClose: () => void } const SecondStep = ({ visible, factorName, factor: outerFactor, isLoading, onClose, }: SecondStepProps) => { const queryClient = useQueryClient() const [lastVisitedOrganization] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, '' ) const FormSchema = z.object({ code: z.string().min(1, 'Please provide a code from your authenticator app'), }) const form = useForm>({ resolver: zodResolver(FormSchema as any), defaultValues: { code: '' }, mode: 'onChange', }) const [factor, setFactor] = useState<{ id: string; type: 'totp'; totp: TOTP } | null>(null) const { mutate: unenroll } = useMfaUnenrollMutation({ onSuccess: () => onClose() }) const { mutate: challengeAndVerify, isPending: isVerifying } = useMfaChallengeAndVerifyMutation({ onError: (error) => { toast.error(`Failed to add a second factor authentication: ${error?.message}`) }, onSuccess: async () => { if (lastVisitedOrganization) { await queryClient.invalidateQueries({ queryKey: organizationKeys.members(lastVisitedOrganization), }) } toast.success(`Successfully added a second factor authentication`) onClose() }, }) const onSubmit: SubmitHandler> = async (values) => { if (!factor) return toast.error('Factor required') challengeAndVerify({ factorId: factor.id, code: values.code }) } // this useEffect is to keep the factor until a new one comes. This is a fix to an issue which // happens when closing the modal, the outer factor is reset to null too soon and the modal // removes a big div mid transition. useEffect(() => { if (outerFactor && factor?.id !== outerFactor.id) { setFactor(outerFactor) form.reset({ code: '' }) } }, [outerFactor]) return ( { // If a factor has been created (but not verified), unenroll it. This will be run as a // side effect so that it's not confusing to the user why the modal stays open while // unenrolling. if (factor) unenroll({ factorId: factor.id }) }} onConfirm={form.handleSubmit(onSubmit)} >

Use an authenticator app to scan the following QR code, and provide the code from the app to complete the enrolment.

{isLoading && (
)} {factor && (
{factor.totp.uri}
} />
( )} />
)}
) }