import { Form } from '@inertiajs/react';
import { REGEXP_ONLY_DIGITS } from 'input-otp';
import { Check, Copy, ScanLine } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import AlertError from '@/components/alert-error';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from '@/components/ui/input-otp';
import { Spinner } from '@/components/ui/spinner';
import { useAppearance } from '@/hooks/use-appearance';
import { useClipboard } from '@/hooks/use-clipboard';
import { OTP_MAX_LENGTH } from '@/hooks/use-two-factor-auth';
import { useTranslation } from '@/lib/translations';
import { confirm } from '@/routes/two-factor';
function GridScanIcon() {
return (
{Array.from({ length: 5 }, (_, i) => (
))}
{Array.from({ length: 5 }, (_, i) => (
))}
);
}
function TwoFactorSetupStep({
qrCodeSvg,
manualSetupKey,
buttonText,
onNextStep,
errors,
}: {
qrCodeSvg: string | null;
manualSetupKey: string | null;
buttonText: string;
onNextStep: () => void;
errors: string[];
}) {
const { resolvedAppearance } = useAppearance();
const [copiedText, copy] = useClipboard();
const { t } = useTranslation();
const IconComponent = copiedText === manualSetupKey ? Check : Copy;
return (
<>
{errors?.length ? (
) : (
<>
{t('or, enter the code manually')}
{!manualSetupKey ? (
) : (
<>
>
)}
>
)}
>
);
}
function TwoFactorVerificationStep({
onClose,
onBack,
}: {
onClose: () => void;
onBack: () => void;
}) {
const [code, setCode] = useState('');
const pinInputContainerRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
setTimeout(() => {
pinInputContainerRef.current?.querySelector('input')?.focus();
}, 0);
}, []);
return (
);
}
type Props = {
isOpen: boolean;
onClose: () => void;
requiresConfirmation: boolean;
twoFactorEnabled: boolean;
qrCodeSvg: string | null;
manualSetupKey: string | null;
clearSetupData: () => void;
fetchSetupData: () => Promise;
errors: string[];
};
export default function TwoFactorSetupModal({
isOpen,
onClose,
requiresConfirmation,
twoFactorEnabled,
qrCodeSvg,
manualSetupKey,
clearSetupData,
fetchSetupData,
errors,
}: Props) {
const [showVerificationStep, setShowVerificationStep] =
useState(false);
const { t } = useTranslation();
const modalConfig = useMemo<{
title: string;
description: string;
buttonText: string;
}>(() => {
if (twoFactorEnabled) {
return {
title: t('Two-factor authentication enabled'),
description: t(
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
),
buttonText: t('Close'),
};
}
if (showVerificationStep) {
return {
title: t('Verify authentication code'),
description: t(
'Enter the 6-digit code from your authenticator app',
),
buttonText: t('Continue'),
};
}
return {
title: t('Enable two-factor authentication'),
description: t(
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
),
buttonText: t('Continue'),
};
}, [showVerificationStep, t, twoFactorEnabled]);
const resetModalState = useCallback(() => {
setShowVerificationStep(false);
clearSetupData();
}, [clearSetupData]);
const handleClose = useCallback(() => {
resetModalState();
onClose();
}, [onClose, resetModalState]);
const handleModalNextStep = useCallback(() => {
if (requiresConfirmation) {
setShowVerificationStep(true);
return;
}
handleClose();
}, [requiresConfirmation, handleClose]);
const fetchSetupDataRef = useRef(fetchSetupData);
useEffect(() => {
fetchSetupDataRef.current = fetchSetupData;
}, [fetchSetupData]);
useEffect(() => {
if (isOpen && !qrCodeSvg) {
fetchSetupDataRef.current();
}
}, [isOpen, qrCodeSvg]);
return (
);
}