import { useQueryClient } from '@tanstack/react-query' import { Dispatch, SetStateAction, useEffect, useState } from 'react' import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS } from 'common' import InformationBox from 'components/ui/InformationBox' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' 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' import { Input } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' interface AddNewFactorModalProps { visible: boolean onClose: () => void } const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => { // Generate a name with a number between 0 and 1000 const [name, setName] = useState(`App ${Math.floor(Math.random() * 1000)}`) const { data, mutate: enroll, isLoading: isEnrolling, reset } = useMfaEnrollMutation() useEffect(() => { // reset has to be called because the state is kept between if the process is canceled during // the second step. if (!visible) { setName(`App ${Math.floor(Math.random() * 1000)}`) reset() } }, [reset, visible]) return ( <> } isLoading={isEnrolling} onClose={onClose} /> ) } interface FirstStepProps { visible: boolean name: string setName: Dispatch> enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void isEnrolling: boolean onClose: () => void } const FirstStep = ({ visible, name, enroll, setName, isEnrolling, onClose }: FirstStepProps) => { return ( { enroll({ factorType: 'totp', friendlyName: name, }) }} > setName(e.target.value)} /> ) } interface SecondStepProps { visible: boolean factorName: string factor?: { id: string type: 'totp' totp: { qr_code: string secret: string uri: string } } isLoading: boolean onClose: () => void } const SecondStep = ({ visible, factorName, factor: outerFactor, isLoading, onClose, }: SecondStepProps) => { const [code, setCode] = useState('') const queryClient = useQueryClient() const [lastVisitedOrganization] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, '' ) const { mutate: unenroll } = useMfaUnenrollMutation({ onSuccess: () => onClose() }) const { mutate: challengeAndVerify, isLoading: isVerifying } = useMfaChallengeAndVerifyMutation({ onError: (error) => { toast.error(`Failed to add a second factor authentication: ${error?.message}`) }, onSuccess: async () => { if (lastVisitedOrganization) { await queryClient.invalidateQueries(organizationKeys.members(lastVisitedOrganization)) } toast.success(`Successfully added a second factor authentication`) onClose() }, }) const [factor, setFactor] = useState<{ id: string type: 'totp' totp: { qr_code: string secret: string uri: string } } | null>(null) // 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) } }, [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={() => factor && challengeAndVerify({ factorId: factor.id, code })} >
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}
} />
setCode(e.target.value)} />
)}
) } export default AddNewFactorModal