import { getRelayListForUser } from '@nostr-dev-kit/ndk' import { QRCodeSVG } from 'qrcode.react' import React, { Dispatch, ReactNode, SetStateAction, useCallback, useMemo, useState } from 'react' import Countdown, { CountdownRenderProps } from 'react-countdown' import { toast } from 'react-toastify' import { ZapController } from '../controllers' import { useAppSelector, useDidMount, useNDKContext } from '../hooks' import '../styles/popup.css' import { PaymentRequest, UserProfile } from '../types' import { copyTextToClipboard, formatNumber, getTagValue, getZapAmount, unformatNumber } from '../utils' import { LoadingSpinner } from './LoadingSpinner' import { FALLBACK_PROFILE_IMAGE } from 'constants.ts' type PresetAmountProps = { label: string value: number setAmount: Dispatch> } export const PresetAmount = React.memo( ({ label, value, setAmount }: PresetAmountProps) => { return ( ) } ) type ZapPresetsProps = { setAmount: Dispatch> } export const ZapPresets = React.memo(({ setAmount }: ZapPresetsProps) => { return ( <> ) }) type ZapButtonsProps = { disabled: boolean handleGenerateQRCode: () => void handleSend: () => void } export const ZapButtons = ({ disabled, handleGenerateQRCode, handleSend }: ZapButtonsProps) => { return (
) } type ZapQRProps = { paymentRequest: PaymentRequest handleClose: () => void handleQRExpiry: () => void setTotalZapAmount?: Dispatch> setHasZapped?: Dispatch> } export const ZapQR = React.memo( ({ paymentRequest, handleClose, handleQRExpiry, setTotalZapAmount, setHasZapped }: ZapQRProps) => { const { ndk } = useNDKContext() useDidMount(() => { ZapController.getInstance() .pollZapReceipt(paymentRequest, ndk) .then((zapReceipt) => { toast.success(`Successfully sent sats!`) if (setTotalZapAmount) { const amount = getZapAmount(zapReceipt) setTotalZapAmount((prev) => prev + amount) if (setHasZapped) setHasZapped(true) } }) .catch((err) => { toast.error(err.message || err) }) .finally(() => { handleClose() }) }) const onQrCodeClicked = async () => { if (!paymentRequest) return const zapController = ZapController.getInstance() if (await zapController.isWeblnProviderExists()) { zapController.sendPayment(paymentRequest.pr) } else { console.warn('Webln provider not present') const href = `lightning:${paymentRequest.pr}` const a = document.createElement('a') a.href = href a.click() } } return (
) } ) const MAX_POLLING_TIME = 2 * 60 * 1000 // 2 minutes in milliseconds const renderer = ({ minutes, seconds }: CountdownRenderProps) => ( {minutes}:{seconds} ) type TimerProps = { onTimerExpired: () => void } const Timer = React.memo(({ onTimerExpired }: TimerProps) => { const expiryTime = useMemo(() => { return Date.now() + MAX_POLLING_TIME }, []) return (
) }) type ZapPopUpProps = { title: string labelDescriptionMain?: ReactNode receiver: string eventId?: string aTag?: string notCloseAfterZap?: boolean lastNode?: ReactNode setTotalZapAmount?: Dispatch> setHasZapped?: Dispatch> handleClose: () => void } export const ZapPopUp = ({ title, labelDescriptionMain, receiver, eventId, aTag, lastNode, notCloseAfterZap, setTotalZapAmount, setHasZapped, handleClose }: ZapPopUpProps) => { const { ndk, findMetadata } = useNDKContext() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [amount, setAmount] = useState(0) const [message, setMessage] = useState('') const [paymentRequest, setPaymentRequest] = useState() const userState = useAppSelector((state) => state.user) const handleAmountChange = (event: React.ChangeEvent) => { const unformattedValue = unformatNumber(event.target.value) setAmount(unformattedValue) } const generatePaymentRequest = useCallback(async (): Promise => { let userHexKey: string setIsLoading(true) setLoadingSpinnerDesc('Getting user pubkey') if (userState.auth && userState.user?.pubkey) { userHexKey = userState.user.pubkey as string } else { userHexKey = (await window.nostr?.getPublicKey()) as string } if (!userHexKey) { setIsLoading(false) toast.error('Could not get pubkey') return null } setLoadingSpinnerDesc('finding receiver metadata') const receiverMetadata = await findMetadata(receiver) if (!receiverMetadata?.lud16) { setIsLoading(false) toast.error('Lighting address (lud16) is missing in receiver metadata!') return null } if (!receiverMetadata?.pubkey) { setIsLoading(false) toast.error('pubkey is missing in receiver metadata!') return null } // Find the receiver's read relays. const receiverRelays = await getRelayListForUser(receiver, ndk) .then((ndkRelayList) => { if (ndkRelayList) return ndkRelayList.readRelayUrls return [] // Return an empty array if ndkRelayList is undefined }) .catch((err) => { console.error( `An error occurred in getting zap receiver's read relays`, err ) return [] as string[] }) const zapController = ZapController.getInstance() setLoadingSpinnerDesc('Creating zap request') return await zapController .getLightningPaymentRequest( receiverMetadata.lud16, amount, receiverMetadata.pubkey as string, receiverRelays, userHexKey, message, eventId, aTag ) .catch((err) => { toast.error(err.message || err) return null }) .finally(() => { setIsLoading(false) }) }, [amount, message, userState, receiver, eventId, aTag]) const handleGenerateQRCode = async () => { const pr = await generatePaymentRequest() if (!pr) return setPaymentRequest(pr) } const handleSend = useCallback(async () => { const pr = await generatePaymentRequest() if (!pr) return setIsLoading(true) setLoadingSpinnerDesc('Sending payment!') const zapController = ZapController.getInstance() if (await zapController.isWeblnProviderExists()) { await zapController .sendPayment(pr.pr) .then(() => { toast.success(`Successfully sent ${amount} sats!`) if (setTotalZapAmount) { setTotalZapAmount((prev) => prev + amount) if (setHasZapped) setHasZapped(true) } if (!notCloseAfterZap) { handleClose() } }) .catch((err) => { toast.error(err.message || err) }) } else { toast.warn('Webln is not present. Use QR code to send zap.') setPaymentRequest(pr) } setIsLoading(false) }, [ amount, notCloseAfterZap, handleClose, generatePaymentRequest, setTotalZapAmount, setHasZapped ]) const handleQRExpiry = useCallback(() => { setPaymentRequest(undefined) }, []) const handleQRClose = useCallback(() => { setPaymentRequest(undefined) setIsLoading(false) if (!notCloseAfterZap) { handleClose() } }, [notCloseAfterZap, handleClose]) return ( <> {isLoading && }

{title}

{labelDescriptionMain}
setMessage(e.target.value)} />
{paymentRequest && ( )} {lastNode}
) } type ZapSplitProps = { pubkey: string eventId?: string aTag?: string setTotalZapAmount?: Dispatch> setHasZapped?: Dispatch> handleClose: () => void } export const ZapSplit = ({ pubkey, eventId, aTag, setTotalZapAmount, setHasZapped, handleClose }: ZapSplitProps) => { const { ndk, findMetadata } = useNDKContext() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [amount, setAmount] = useState(0) const [message, setMessage] = useState('') const [authorPercentage, setAuthorPercentage] = useState(90) const [adminPercentage, setAdminPercentage] = useState(10) const [author, setAuthor] = useState() const [admin, setAdmin] = useState() const userState = useAppSelector((state) => state.user) const [invoices, setInvoices] = useState>() useDidMount(async () => { findMetadata(pubkey).then((res) => { setAuthor(res) }) const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',') findMetadata(adminNpubs[0]).then((res) => { setAdmin(res) }) }) const handleAuthorPercentageChange = ( e: React.ChangeEvent ) => { const newValue = parseInt(e.target.value) setAuthorPercentage(newValue) setAdminPercentage(100 - newValue) } const handleAdminPercentageChange = ( e: React.ChangeEvent ) => { const newValue = parseInt(e.target.value) setAdminPercentage(newValue) setAuthorPercentage(100 - newValue) // Update the other slider to maintain 100% } const handleAmountChange = (event: React.ChangeEvent) => { const unformattedValue = unformatNumber(event.target.value) setAmount(unformattedValue) } const generatePaymentInvoices = async () => { if (!amount) return null let userHexKey: string setIsLoading(true) setLoadingSpinnerDesc('Getting user pubkey') if (userState.auth && userState.user?.pubkey) { userHexKey = userState.user.pubkey as string } else { userHexKey = (await window.nostr?.getPublicKey()) as string } if (!userHexKey) { setIsLoading(false) toast.error('Could not get pubkey') return null } const adminShare = Math.floor((amount * adminPercentage) / 100) const authorShare = amount - adminShare const zapController = ZapController.getInstance() const invoices = new Map() if (authorShare > 0 && author?.pubkey && author?.lud16) { // Find the receiver's read relays. const authorRelays = await getRelayListForUser( author.pubkey as string, ndk ) .then((ndkRelayList) => { if (ndkRelayList) return ndkRelayList.readRelayUrls return [] // Return an empty array if ndkRelayList is undefined }) .catch((err) => { console.error( `An error occurred in getting zap receiver's read relays`, err ) return [] as string[] }) setLoadingSpinnerDesc('Generating invoice for author') const invoice = await zapController .getLightningPaymentRequest( author.lud16, authorShare, author.pubkey as string, authorRelays, userHexKey, message, eventId, aTag ) .catch((err) => { toast.error(err.message || err) return null }) if (invoice) { invoices.set('author', invoice) } } if (adminShare > 0 && admin?.pubkey && admin?.lud16) { // Find the receiver's read relays. const adminRelays = await getRelayListForUser(admin.pubkey as string, ndk) .then((ndkRelayList) => { if (ndkRelayList) return ndkRelayList.readRelayUrls return [] // Return an empty array if ndkRelayList is undefined }) .catch((err) => { console.error( `An error occurred in getting zap receiver's read relays`, err ) return [] as string[] }) setLoadingSpinnerDesc('Generating invoice for site owner') const invoice = await zapController .getLightningPaymentRequest( admin.lud16, adminShare, admin.pubkey as string, adminRelays, userHexKey, message, eventId, aTag ) .catch((err) => { toast.error(err.message || err) return null }) if (invoice) { invoices.set('admin', invoice) } } setIsLoading(false) return invoices } const handleGenerateQRCode = async () => { const paymentInvoices = await generatePaymentInvoices() if (!paymentInvoices) return setInvoices(paymentInvoices) } const handleSend = async () => { const paymentInvoices = await generatePaymentInvoices() if (!paymentInvoices) return setIsLoading(true) setLoadingSpinnerDesc('Sending payment!') const zapController = ZapController.getInstance() if (await zapController.isWeblnProviderExists()) { const authorInvoice = paymentInvoices.get('author') if (authorInvoice) { setLoadingSpinnerDesc('Sending payment to author') const sats = parseInt(getTagValue(authorInvoice, 'amount')![0]) / 1000 await zapController .sendPayment(authorInvoice.pr) .then(() => { toast.success(`Successfully sent ${sats} sats to author!`) if (setTotalZapAmount) { setTotalZapAmount((prev) => prev + sats) if (setHasZapped) setHasZapped(true) } }) .catch((err) => { toast.error(err.message || err) }) } const adminInvoice = paymentInvoices.get('admin') if (adminInvoice) { setLoadingSpinnerDesc('Sending payment to site owner') const sats = parseInt(getTagValue(adminInvoice, 'amount')![0]) / 1000 await zapController .sendPayment(adminInvoice.pr) .then(() => { toast.success(`Successfully sent ${sats} sats to site owner!`) }) .catch((err) => { toast.error(err.message || err) }) } handleClose() } else { toast.warn('Webln is not present. Use QR code to send zap.') setInvoices(paymentInvoices) } } const removeInvoice = (key: string) => { setInvoices((prev) => { const newMap = new Map(prev) newMap.delete(key) return newMap }) } const displayQR = () => { if (!invoices) return null const authorInvoice = invoices.get('author') if (authorInvoice) { return ( removeInvoice('author')} handleQRExpiry={() => removeInvoice('author')} setTotalZapAmount={setTotalZapAmount} setHasZapped={setHasZapped} /> ) } const adminInvoice = invoices.get('admin') if (adminInvoice) { return ( { removeInvoice('admin') handleClose() }} handleQRExpiry={() => removeInvoice('admin')} /> ) } return null } const authorName = author?.displayName || author?.name || '[name not set up]' const adminName = admin?.displayName || admin?.name || '[name not set up]' return ( <> {isLoading && }

Tip/Zap

setMessage(e.target.value)} />

{authorName}

{author?.nip05 && (

{author.nip05}

)}

{authorPercentage}%

This goes to show your appreciation to the mod creator!

{adminName}

{admin?.nip05 && (

)}

{adminPercentage}%

Help with the development, maintenance, management, and growth of DEG Mods.

{displayQR()}
) }