| // src/components/settings/TwoFactorSetupModal.tsx | |
| import { useState, useEffect } from "react"; | |
| import { X, Shield, CheckCircle, Loader2, Copy, RefreshCw } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| interface TwoFactorSetupModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| secret: string; | |
| qrCode: string; | |
| code: string; | |
| setCode: (code: string) => void; | |
| loading: boolean; | |
| error: string; | |
| success: string; | |
| onVerify: () => void; | |
| onDisable?: () => void; | |
| backupCodes?: string[]; | |
| onRegenerateBackupCodes?: () => void; | |
| } | |
| export function TwoFactorSetupModal({ | |
| isOpen, | |
| onClose, | |
| secret, | |
| qrCode, | |
| code, | |
| setCode, | |
| loading, | |
| error, | |
| success, | |
| onVerify, | |
| onDisable, | |
| backupCodes = [], | |
| onRegenerateBackupCodes, | |
| }: TwoFactorSetupModalProps) { | |
| const [step, setStep] = useState<"intro" | "scan" | "verify" | "done">("intro"); | |
| const [showBackupCodes, setShowBackupCodes] = useState(false); | |
| // Reset state when modal opens | |
| useEffect(() => { | |
| if (isOpen) { | |
| if (backupCodes.length > 0) { | |
| setStep("done"); | |
| setShowBackupCodes(true); | |
| } else if (secret && qrCode) { | |
| setStep("verify"); | |
| } else { | |
| setStep("intro"); | |
| } | |
| setCode(""); | |
| } | |
| }, [isOpen, secret, qrCode, backupCodes]); | |
| if (!isOpen) return null; | |
| const handleEnable = () => { | |
| setStep("scan"); | |
| }; | |
| const handleVerify = async () => { | |
| await onVerify(); | |
| if (success && backupCodes.length > 0) { | |
| setStep("done"); | |
| setShowBackupCodes(true); | |
| } | |
| }; | |
| const copyBackupCodes = () => { | |
| navigator.clipboard.writeText(backupCodes.join("\n")); | |
| toast.success("Backup codes copied!"); | |
| }; | |
| const handleClose = () => { | |
| setStep("intro"); | |
| onClose(); | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onClick={handleClose}> | |
| <div | |
| className="w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-6 shadow-xl" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <div className="mb-4 flex items-center justify-between"> | |
| <h2 className="text-xl font-bold flex items-center gap-2"> | |
| <Shield className="h-6 w-6 text-primary" /> | |
| Two‑Factor Authentication | |
| </h2> | |
| <button onClick={handleClose} className="text-gray-500 hover:text-gray-700"> | |
| <X className="h-5 w-5" /> | |
| </button> | |
| </div> | |
| {step === "intro" && ( | |
| <div className="space-y-4"> | |
| <p className="text-sm text-gray-600 dark:text-gray-400"> | |
| Add an extra layer of security to your account by requiring a code from your authenticator app. | |
| </p> | |
| <button onClick={handleEnable} className="btn-primary w-full"> | |
| Get Started | |
| </button> | |
| </div> | |
| )} | |
| {step === "scan" && ( | |
| <div className="space-y-4"> | |
| <p className="text-sm text-gray-600 dark:text-gray-400"> | |
| Scan this QR code with your authenticator app (e.g., Google Authenticator, Authy). | |
| </p> | |
| {qrCode ? ( | |
| <div className="flex justify-center"> | |
| <img src={qrCode} alt="QR Code" className="h-48 w-48 rounded-xl border" /> | |
| </div> | |
| ) : ( | |
| <div className="flex justify-center py-12"> | |
| <Loader2 className="h-8 w-8 animate-spin text-gray-400" /> | |
| </div> | |
| )} | |
| {secret && ( | |
| <div className="text-center"> | |
| <p className="text-xs text-gray-500">Or enter this secret manually:</p> | |
| <code className="rounded bg-gray-100 px-2 py-1 font-mono text-sm dark:bg-gray-700"> | |
| {secret} | |
| </code> | |
| </div> | |
| )} | |
| <button onClick={() => setStep("verify")} className="btn-primary mt-2 w-full"> | |
| Next | |
| </button> | |
| </div> | |
| )} | |
| {step === "verify" && ( | |
| <div className="space-y-4"> | |
| <p className="text-sm text-gray-600 dark:text-gray-400"> | |
| Enter the 6‑digit code from your authenticator app to verify setup. | |
| </p> | |
| <input | |
| type="text" | |
| maxLength={6} | |
| placeholder="000000" | |
| value={code} | |
| onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))} | |
| className="w-full rounded-xl border p-3 text-center text-2xl tracking-[0.5em] dark:bg-gray-700" | |
| /> | |
| {error && <p className="text-sm text-red-500">{error}</p>} | |
| {success && <p className="text-sm text-green-600">{success}</p>} | |
| <button | |
| onClick={handleVerify} | |
| disabled={loading || code.length !== 6} | |
| className="btn-primary flex w-full items-center justify-center gap-2" | |
| > | |
| {loading && <Loader2 className="h-4 w-4 animate-spin" />} | |
| Verify & Enable | |
| </button> | |
| </div> | |
| )} | |
| {step === "done" && ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center gap-2 text-green-600"> | |
| <CheckCircle className="h-5 w-5" /> | |
| <span className="font-semibold">Two‑factor authentication is enabled</span> | |
| </div> | |
| {showBackupCodes && backupCodes.length > 0 && ( | |
| <div className="space-y-2"> | |
| <p className="text-sm text-gray-600 dark:text-gray-400"> | |
| Save these backup codes in a safe place. You can use them to sign in if you lose access to your authenticator app. | |
| </p> | |
| <div className="grid grid-cols-2 gap-1 rounded-xl bg-gray-100 p-3 font-mono text-sm dark:bg-gray-700"> | |
| {backupCodes.map((code, idx) => ( | |
| <span key={idx} className="text-gray-800 dark:text-gray-200"> | |
| {code} | |
| </span> | |
| ))} | |
| </div> | |
| <div className="flex gap-2"> | |
| <button onClick={copyBackupCodes} className="btn-secondary btn-sm flex items-center gap-1"> | |
| <Copy className="h-4 w-4" /> Copy All | |
| </button> | |
| {onRegenerateBackupCodes && ( | |
| <button onClick={onRegenerateBackupCodes} className="btn-ghost btn-sm flex items-center gap-1"> | |
| <RefreshCw className="h-4 w-4" /> Regenerate | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {onDisable && ( | |
| <button onClick={onDisable} className="btn-danger mt-4 w-full"> | |
| Disable 2FA | |
| </button> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 6.88 kB
- Xet hash:
- 5ef0dce4cb3b4ca926e5949951d393f9743801d4ac04e05e0124f847c5ee4c11
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.