damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs | README | LICENSE

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 }