OTPAuth.tsx (4930B)
1 import { Button } from "./ui/Button"; 2 import { useIntl } from "react-intl"; 3 import { useEffect, useState } from "react"; 4 import { Info, Loader2, Mail } from "lucide-react"; 5 import { InputOTP6Digits } from "@/components/ui/InputOTP"; 6 import { ErrorDialog } from "./ErrorDialog"; 7 import { useTimeout } from "usehooks-ts"; 8 import CopiableUrl from "./ui/CopiableUrl"; 9 10 export interface OTPAuthProps { 11 pubkey: string | null 12 verifyOTP: (otp: string) => void 13 sendOTP: () => void 14 otpVerified: boolean 15 setOTPVerified: (verified: boolean) => void 16 otpInvalid: boolean 17 setOTPInvalid: (invalid: boolean) => void 18 setError?: (error: string) => void 19 disabled?: boolean 20 } 21 22 export function OTPAuth(props: OTPAuthProps) { 23 const intl = useIntl() 24 const { setError } = props 25 const [otp, setOTP] = useState<string>("") 26 const [showTroubleshootingMessage, setShowTroubleshootingMessage] = useState<boolean>(false) 27 useTimeout(() => setShowTroubleshootingMessage(true), otp.length == 0 ? 10000 : null) 28 29 // MARK: - Functions 30 31 const completeOTP = async () => { 32 if (!otp) { 33 return 34 } 35 props.verifyOTP(otp) 36 } 37 38 // MARK: - Effects and hooks 39 40 useEffect(() => { 41 setOTP("") 42 }, [props.pubkey]) 43 44 useEffect(() => { 45 if (otp.length != 6) { 46 props.setOTPInvalid(false) 47 props.setOTPVerified(false) 48 } 49 if(otp.length > 0) { 50 setShowTroubleshootingMessage(false) 51 } 52 }, [otp]) 53 54 // MARK: - Render 55 56 return (<> 57 <div className="text-purple-200/50 font-normal flex items-center gap-2 rounded-full px-6 py-2 justify-center mt-2 mb-2"> 58 <div className="flex flex-col items-center"> 59 <Mail className="w-10 h-10 shrink-0 text-purple-100 mb-3" /> 60 <div className="text-purple-200/90 font-semibold text-md whitespace-pre-line text-center"> 61 {intl.formatMessage({ id: "purple.login.otp-sent", defaultMessage: "We sent you a code via a Nostr direct message.\n Please enter it below" })} 62 </div> 63 </div> 64 </div> 65 <div className="mx-auto flex justify-center mb-4"> 66 <InputOTP6Digits value={otp} onChange={setOTP} onComplete={() => completeOTP()} disabled={props.disabled} /> 67 </div> 68 {!props.otpVerified && !props.otpInvalid && otp.length >= 6 && ( 69 <div className="flex flex-col items-center justify-center w-full mb-2"> 70 <div className="text-purple-200/50 font-normal text-sm flex items-center"> 71 <Loader2 className="w-4 h-4 mr-2 animate-spin" /> 72 Loading... 73 </div> 74 </div> 75 )} 76 {props.otpInvalid && (<div className="my-4 w-full flex flex-col gap-2"> 77 <div className="text-red-500 font-normal text-sm text-center"> 78 {intl.formatMessage({ id: "purple.login.otp-invalid", defaultMessage: "Invalid or expired OTP. Please try again." })} 79 </div> 80 <Button variant="default" className="w-full" onClick={() => { 81 props.sendOTP() 82 props.setOTPInvalid(false) 83 setOTP("") 84 }}>Resend OTP</Button> 85 </div>)} 86 {showTroubleshootingMessage ? ( 87 <InfoLabel 88 heading={intl.formatMessage({ id: "purple.otp.troubleshooting.heading", defaultMessage: "Didn't receive the OTP?" })} 89 message={<> 90 <p className="mb-3"> 91 {intl.formatMessage({ id: "purple.otp.troubleshooting.message", defaultMessage: "If you don't see the OTP code, try the following steps:" })} 92 </p> 93 <ol className="list-decimal list-inside text-xs space-y-1"> 94 <li>{intl.formatMessage({ id: "purple.otp.troubleshooting.step1", defaultMessage: "Check your DM request tab (if you are on Damus)" })}</li> 95 <li>{intl.formatMessage({ id: "purple.otp.troubleshooting.step2", defaultMessage: "Ensure your Nostr client is connected to our relay listed below:" })}</li> 96 </ol> 97 <CopiableUrl url={"wss://relay.damus.io"} className="mt-2"/> 98 </>} 99 /> 100 ) : 101 <InfoLabel 102 heading={intl.formatMessage({ id: "purple.login.stay-safe.title", defaultMessage: "Stay safe" })} 103 message={intl.formatMessage({ id: "purple.login.stay-safe.message", defaultMessage: "We will never ask you for your nsec or any other sensitive information via Nostr DMs. Beware of impersonators. Please do not share your OTP code with anyone." })} 104 /> 105 } 106 </>) 107 } 108 109 110 function InfoLabel({ heading, message }: { heading: string, message: React.ReactNode }) { 111 return ( 112 <div className="text-purple-200/70 text-normal text-left font-semibold flex flex-col md:flex-row gap-3 rounded-lg bg-purple-200/10 p-3 items-center md:items-start"> 113 <Info className="w-6 h-6 shrink-0 mt-0 md:mt-1" /> 114 <div className="flex flex-col text-center md:text-left"> 115 <span className="text-normal md:text-lg mb-2"> 116 {heading} 117 </span> 118 <span className="text-xs text-purple-200/50 whitespace-pre-line"> 119 {message} 120 </span> 121 </div> 122 </div> 123 ) 124 }