damus.io

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

Step3Payment.tsx (7784B)


      1 import { ArrowUpRight, CheckCircle, Copy, Loader2 } from "lucide-react";
      2 import { Button } from "@/components/ui/Button";
      3 import { useIntl } from "react-intl";
      4 import Link from "next/link";
      5 import { useEffect, useState } from "react";
      6 import { QRCodeSVG } from 'qrcode.react';
      7 import { useInterval } from 'usehooks-ts'
      8 import Lnmessage from 'lnmessage'
      9 import { LNCheckout } from "./Types";
     10 import { StepHeader } from "./StepHeader";
     11 import CopiableUrl from "@/components/ui/CopiableUrl";
     12 
     13 export interface Step3PaymentProps {
     14   lnCheckout: LNCheckout | null
     15   setLNCheckout: (checkout: LNCheckout | null) => void
     16   setError: (error: string) => void
     17   successView: React.ReactNode
     18 }
     19 
     20 export function Step3Payment(props: Step3PaymentProps) {
     21   const intl = useIntl()
     22   const { lnCheckout, setLNCheckout } = props
     23   const [lnInvoicePaid, setLNInvoicePaid] = useState<boolean | undefined>(undefined) // Whether the ln invoice has been paid
     24   const [waitingForInvoice, setWaitingForInvoice] = useState<boolean>(false) // Whether we are waiting for a response from the LN node about the invoice
     25   
     26   const [lnConnectionRetryCount, setLnConnectionRetryCount] = useState<number>(0)  // The number of times we have tried to connect to the LN node
     27   const lnConnectionRetryLimit = 5  // The maximum number of times we will try to connect to the LN node before displaying an error
     28   const [lnWaitinvoiceRetryCount, setLnWaitinvoiceRetryCount] = useState<number>(0)  // The number of times we have tried to check the invoice status
     29   const lnWaitinvoiceRetryLimit = 5  // The maximum number of times we will try to check the invoice status before displaying an error
     30   
     31   const step1Done = props.lnCheckout?.product_template_name != null
     32   const step2Done = props.lnCheckout?.verified_pubkey != null
     33   const step3Done = props.lnCheckout?.invoice?.paid == true
     34 
     35   // MARK: - Functions
     36 
     37   const checkLNInvoice = async () => {
     38     console.log("Checking LN invoice...")
     39     if (!lnCheckout?.invoice?.bolt11) {
     40       return
     41     }
     42     let ln = null
     43     try {
     44       ln = new Lnmessage({
     45         // The public key of the node you would like to connect to
     46         remoteNodePublicKey: lnCheckout.invoice.connection_params.nodeid,
     47         // The websocket proxy address of the node
     48         wsProxy: `wss://${lnCheckout.invoice.connection_params.ws_proxy_address}`,
     49         // The IP address of the node
     50         ip: lnCheckout.invoice.connection_params.address,
     51         // Protocol to use when connecting to the node
     52         wsProtocol: 'wss:',
     53         port: 9735,
     54       })
     55       // TODO: This is a workaround due to a limitation in LNMessage URL formatting: (https://github.com/aaronbarnardsound/lnmessage/issues/52)
     56       ln.wsUrl = `wss://${lnCheckout.invoice.connection_params.ws_proxy_address}/${lnCheckout.invoice.connection_params.address}`
     57       await ln.connect()
     58       setWaitingForInvoice(true)  // Indicate that we are waiting for a response from the LN node
     59     }
     60     catch (e) {
     61       console.error(e)
     62       if (lnConnectionRetryCount >= lnConnectionRetryLimit) {
     63         props.setError("Failed to connect to the Lightning node. Please refresh this page, and try again in a few minutes. If the problem persists, please contact support.")
     64       }
     65       else {
     66         setLnConnectionRetryCount(lnConnectionRetryCount + 1)
     67       }
     68       return
     69     }
     70 
     71     try {
     72       if (!ln) { return }
     73       const res: any = await ln.commando({
     74         method: 'waitinvoice',
     75         params: { label: lnCheckout.invoice.label },
     76         rune: lnCheckout.invoice.connection_params.rune,
     77       })
     78       setWaitingForInvoice(false)  // Indicate that we are no longer waiting for a response from the LN node
     79       setLNInvoicePaid(!res.error)
     80       if (res.error) {
     81         console.error(res.error)
     82         props.setError("The lightning payment failed. If you haven't paid yet, please start a new checkout from the beginning and try again. If you have already paid, please copy the reference ID shown below and contact support.")
     83       }
     84     } catch (e) {
     85       setWaitingForInvoice(false)  // Indicate that we are no longer waiting for a response from the LN node
     86       console.error(e)
     87       if (lnWaitinvoiceRetryCount >= lnWaitinvoiceRetryLimit) {
     88         props.setError("There was an error checking the lightning payment status. If you haven't paid yet, please wait a few minutes, refresh the page, and try again. If you have already paid, please copy the reference ID shown below and contact support.")
     89       }
     90       else {
     91         setLnWaitinvoiceRetryCount(lnWaitinvoiceRetryCount + 1)
     92       }
     93     }
     94   }
     95 
     96   const tellServerToCheckLNInvoice = async () => {
     97     try {
     98       const response = await fetch(process.env.NEXT_PUBLIC_PURPLE_API_BASE_URL + "/ln-checkout/" + lnCheckout?.id + "/check-invoice", {
     99         method: 'POST',
    100         headers: {
    101           'Content-Type': 'application/json'
    102         },
    103       })
    104       const data: LNCheckout = await response.json()
    105       setLNCheckout(data)
    106     }
    107     catch (e) {
    108       console.error(e)
    109       props.setError("Failed to finalize checkout. Please try refreshing the page. If the error persists, please copy the reference ID shown below and contact support.")
    110     }
    111   }
    112 
    113   const pollState = async () => {
    114     if (!lnCheckout) {
    115       return
    116     }
    117     if (lnCheckout.invoice && !lnCheckout.invoice?.paid && !waitingForInvoice) {
    118       checkLNInvoice()
    119     }
    120   }
    121 
    122   // MARK: - Effects and hooks
    123 
    124   // Keep checking the state of things when needed
    125   useInterval(pollState, 1000)
    126 
    127   // Tell server to check the invoice as soon as we notice it has been paid
    128   useEffect(() => {
    129     if (lnInvoicePaid === true) {
    130       tellServerToCheckLNInvoice()
    131     }
    132   }, [lnInvoicePaid])
    133 
    134   // MARK: - Render
    135 
    136   return (<>
    137       <StepHeader
    138         stepNumber={3}
    139         title={intl.formatMessage({ id: "purple.checkout.step-3", defaultMessage: "Lightning payment" })}
    140         done={lnCheckout?.invoice?.paid === true}
    141         active={lnCheckout?.verified_pubkey != null}
    142       />
    143       {lnCheckout?.invoice?.bolt11 && !lnCheckout?.invoice?.paid &&
    144         <>
    145           <QRCodeSVG value={"lightning:" + lnCheckout.invoice.bolt11} className="mt-6 w-[300px] h-[300px] max-w-full max-h-full mx-auto mb-6 border-[5px] border-white bg-white" />
    146           {/* Shows the bolt11 in for copy-paste with a copy and paste button */}
    147           <CopiableUrl url={lnCheckout.invoice.bolt11} />
    148           <Link href={"lightning:" + lnCheckout.invoice.bolt11} className="w-full md:w-auto opacity-70 hover:opacity-100 transition mt-4">
    149             <Button variant="link" className="w-full text-sm">
    150               {intl.formatMessage({ id: "purple.checkout.open-in-wallet", defaultMessage: "Open in wallet" })}
    151               <ArrowUpRight className="text-damuspink-600 ml-2" />
    152             </Button>
    153           </Link>
    154           <div className="mt-6 text-purple-200/50 font-normal text-sm text-center flex justify-center">
    155             <Loader2 className="w-4 h-4 mr-2 animate-spin" />
    156             {intl.formatMessage({ id: "purple.checkout.waiting-for-payment", defaultMessage: "Waiting for payment" })}
    157           </div>
    158         </>
    159       }
    160       {/* We use the lnCheckout object to check payment status (NOT lnInvoicePaid) to display the confirmation message, because the server is the ultimate source of truth */}
    161       {lnCheckout?.invoice?.paid && lnCheckout?.completed && (
    162         <div className="flex flex-col items-center justify-center gap-3 mt-6">
    163           <CheckCircle className="w-16 h-16 text-green-500" />
    164           <div className="mt-3 mb-6 text-sm text-center text-green-500 font-bold">
    165             {intl.formatMessage({ id: "purple.checkout.payment-received", defaultMessage: "Payment received" })}
    166           </div>
    167           {props.successView}
    168         </div>
    169       )}
    170   </>)
    171 }