import { zodResolver } from '@hookform/resolvers/zod' import { useQueryClient } from '@tanstack/react-query' import { useAuthError, useFeatureFlags, useFlag } from 'common' import { EyeIcon, EyeOffIcon, LockIcon } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { Button, Form, FormControl, FormField, FormInputGroupInput, InputGroup, InputGroupAddon, InputGroupButton, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import z from 'zod' import { MFA_SUPPORT_EMAIL_HREF } from './SignInMfa.utils' import { AlertError } from '@/components/ui/AlertError' import { useAddLoginEvent } from '@/data/misc/audit-login-mutation' import { useRecoveryCodesStatusQuery } from '@/data/recovery-codes/recovery-codes-status-query' import { useRecoveryCodesVerifyMutation } from '@/data/recovery-codes/recovery-codes-verify-mutation' import { useSignOut } from '@/lib/auth' import { getReturnToPath } from '@/lib/gotrue' import { useTrack } from '@/lib/telemetry/track' const schema = z.object({ code: z.string().min(1, 'Recovery Code is required'), }) const formId = 'sign-in-recovery-code-form' export const SignInRecoveryCodeForm = () => { const router = useRouter() const signOut = useSignOut() const queryClient = useQueryClient() const { hasLoaded } = useFeatureFlags() const enableAuthRecoveryCodes = useFlag('enableAuthRecoveryCodes') const { data: recoveryCodesStatus, isPending: recoveryCodesStatusIsPending, isError: recoveryCodesStatusIsError, error: recoveryCodesStatusError, } = useRecoveryCodesStatusQuery({ enabled: enableAuthRecoveryCodes, }) const track = useTrack() const { mutate: addLoginEvent } = useAddLoginEvent() const form = useForm>({ resolver: zodResolver(schema), defaultValues: { code: '' }, }) const { mutate: verifyRecoveryCode, isPending: isVerifying, isSuccess, } = useRecoveryCodesVerifyMutation({ onSuccess: async () => { track('sign_in', { category: 'account', method: 'recovery-code' }) addLoginEvent({}) await queryClient.resetQueries() router.push(getReturnToPath()) }, }) const onClickLogout = async () => { if (isVerifying) return await signOut() await router.replace('/sign-in') } const onSubmit: SubmitHandler> = async ({ code }) => { if (isVerifying) return verifyRecoveryCode({ code }) } useEffect(() => { // if users wander into this page and he has no recovery code setup or is on platform where it's not enabled if (hasLoaded && (!enableAuthRecoveryCodes || recoveryCodesStatus?.status === 'unenrolled')) { queryClient.resetQueries().then(() => router.push(getReturnToPath())) } }, [hasLoaded, enableAuthRecoveryCodes, recoveryCodesStatus, router, queryClient]) const error = useAuthError() const [isCodeRevealed, setIsCodeRevealed] = useState(false) const formRef = useRef(null) if (error) { return ( } /> ) } return ( <> {recoveryCodesStatusIsPending && } {recoveryCodesStatusIsError && ( )} {recoveryCodesStatus?.status === 'available' && (
( { // If users paste something, it's most probably a valid code so we can safely try submitting it immediately // onPaste is triggered before the paste operation is complete so we have to emulate it to avoid using an unreliable setTimeout event.preventDefault() field.onChange(event.clipboardData.getData('text')) formRef.current?.dispatchEvent( new Event('submit', { cancelable: true, bubbles: true }) ) }} /> setIsCodeRevealed((previous) => !previous)} > {isCodeRevealed ? ( ) : ( )} {isCodeRevealed ? 'Hide code' : 'Show code'} )} />
)}
Unable to sign in?{' '}
) }