chore(refactor): reduce code duplication in zap

This commit is contained in:
daniyal 2024-09-04 11:28:15 +05:00
parent b12887cdf5
commit a85314f0a7
4 changed files with 341 additions and 661 deletions

View File

@ -1,30 +1,22 @@
import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools' import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import { useCallback, useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { import {
MetadataController, MetadataController,
RelayController, RelayController,
UserRelaysType, UserRelaysType
ZapController
} from '../controllers' } from '../controllers'
import { useAppSelector, useDidMount } from '../hooks' import { useAppSelector, useDidMount } from '../hooks'
import { getProfilePageRoute } from '../routes'
import '../styles/author.css' import '../styles/author.css'
import '../styles/innerPage.css' import '../styles/innerPage.css'
import '../styles/socialPosts.css' import '../styles/socialPosts.css'
import { PaymentRequest, UserProfile } from '../types' import { UserProfile } from '../types'
import { import { copyTextToClipboard, log, LogType, now } from '../utils'
copyTextToClipboard,
formatNumber,
log,
LogType,
unformatNumber,
now
} from '../utils'
import { LoadingSpinner } from './LoadingSpinner' import { LoadingSpinner } from './LoadingSpinner'
import { ZapButtons, ZapPresets, ZapQR } from './Zap' import { ZapPopUp } from './Zap'
import { getProfilePageRoute } from '../routes'
import { useNavigate } from 'react-router-dom'
type Props = { type Props = {
pubkey: string pubkey: string
@ -259,7 +251,7 @@ const QRButtonWithPopUp = ({ pubkey }: QRButtonWithPopUpProps) => {
</div> </div>
{isOpen && ( {isOpen && (
<div id='PopUpMainQR' className='popUpMain'> <div className='popUpMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='popUpMainCardWrapper'> <div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'> <div className='popUpMainCard popUpMainCardQR'>
@ -307,122 +299,6 @@ type ZapButtonWithPopUpProps = {
const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => { const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [amount, setAmount] = useState<number>(0)
const [message, setMessage] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
setIsOpen(false)
}, [])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | 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
}
setLoadingSpinnerDesc('Getting admin metadata')
const metadataController = await MetadataController.getInstance()
const authorMetadata = await metadataController.findMetadata(pubkey)
if (!authorMetadata?.lud16) {
setIsLoading(false)
toast.error('Lighting address (lud16) is missing in admin metadata!')
return null
}
if (!authorMetadata?.pubkey) {
setIsLoading(false)
toast.error('pubkey is missing in admin metadata!')
return null
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
authorMetadata.lud16,
amount,
authorMetadata.pubkey as string,
userHexKey,
message
)
.catch((err) => {
toast.error(err.message || err)
return null
})
.finally(() => {
setIsLoading(false)
})
}, [amount, message, userState, pubkey])
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!`)
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, handleClose, generatePaymentRequest])
const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setPaymentRequest(pr)
}
return ( return (
<> <>
@ -442,77 +318,12 @@ const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
</svg> </svg>
</div> </div>
{isOpen && ( {isOpen && (
<div id='PopUpMainZap' className='popUpMain'> <ZapPopUp
<div className='ContainerMain'> title='Tip/Zap'
<div className='popUpMainCardWrapper'> receiver={pubkey}
<div className='popUpMainCard popUpMainCardQR'> handleClose={() => setIsOpen(false)}
<div className='popUpMainCardTop'> />
<div className='popUpMainCardTopInfo'>
<h3>Tip/Zap</h3>
</div>
<div className='popUpMainCardTopClose' onClick={handleClose}>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
style={{ zIndex: 1 }}
>
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z' />
</svg>
</div>
</div>
<div className='pUMCB_Zaps'>
<div className='pUMCB_ZapsInside'>
<div className='pUMCB_ZapsInsideAmount'>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Amount (Satoshis)
</label>
<input
className='inputMain'
type='text'
inputMode='numeric'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Message (optional)
</label>
<input
type='text'
className='inputMain'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleClose}
handleQRExpiry={handleQRExpiry}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
)} )}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</> </>
) )
} }

View File

@ -1,12 +1,20 @@
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import React, { Dispatch, SetStateAction, useMemo } from 'react' import React, {
Dispatch,
ReactNode,
SetStateAction,
useCallback,
useMemo,
useState
} from 'react'
import Countdown, { CountdownRenderProps } from 'react-countdown' import Countdown, { CountdownRenderProps } from 'react-countdown'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { ZapController } from '../controllers' import { MetadataController, ZapController } from '../controllers'
import { useDidMount } from '../hooks' import { useAppSelector, useDidMount } from '../hooks'
import '../styles/popup.css' import '../styles/popup.css'
import { PaymentRequest } from '../types' import { PaymentRequest } from '../types'
import { copyTextToClipboard } from '../utils' import { copyTextToClipboard, formatNumber, unformatNumber } from '../utils'
import { LoadingSpinner } from './LoadingSpinner'
type PresetAmountProps = { type PresetAmountProps = {
label: string label: string
@ -194,3 +202,223 @@ const Timer = React.memo(({ onTimerExpired }: TimerProps) => {
</div> </div>
) )
}) })
type ZapPopUpProps = {
title: string
labelDescriptionMain?: ReactNode
receiver: string
eventId?: string
aTag?: string
notCloseAfterZap?: boolean
lastNode?: ReactNode
handleClose: () => void
}
export const ZapPopUp = ({
title,
labelDescriptionMain,
receiver,
eventId,
aTag,
lastNode,
notCloseAfterZap,
handleClose
}: ZapPopUpProps) => {
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [amount, setAmount] = useState<number>(0)
const [message, setMessage] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | 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
}
setLoadingSpinnerDesc('finding receiver metadata')
const metadataController = await MetadataController.getInstance()
const receiverMetadata = await metadataController.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
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
receiverMetadata.lud16,
amount,
receiverMetadata.pubkey as string,
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 (!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])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const handleQRClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
if (!notCloseAfterZap) {
handleClose()
}
}, [notCloseAfterZap, handleClose])
return (
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<div className='popUpMain'>
<div className='ContainerMain'>
<div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'>
<div className='popUpMainCardTop'>
<div className='popUpMainCardTopInfo'>
<h3>{title}</h3>
</div>
<div className='popUpMainCardTopClose' onClick={handleClose}>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
style={{ zIndex: 1 }}
>
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z' />
</svg>
</div>
</div>
<div className='pUMCB_Zaps'>
<div className='pUMCB_ZapsInside'>
<div className='pUMCB_ZapsInsideAmount'>
<div className='inputLabelWrapperMain'>
{labelDescriptionMain}
<label className='form-label labelMain'>
Amount (Satoshis)
</label>
<input
className='inputMain'
type='text'
inputMode='numeric'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Message (optional)
</label>
<input
type='text'
className='inputMain'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleQRClose}
handleQRExpiry={handleQRExpiry}
/>
)}
{lastNode}
</div>
</div>
</div>
</div>
</div>
</div>
</>
)
}

View File

@ -2,21 +2,18 @@ import {
init as initNostrLogin, init as initNostrLogin,
launch as launchNostrLoginDialog launch as launchNostrLoginDialog
} from 'nostr-login' } from 'nostr-login'
import React, { useCallback, useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { Banner } from '../components/Banner' import { Banner } from '../components/Banner'
import { LoadingSpinner } from '../components/LoadingSpinner' import { ZapPopUp } from '../components/Zap'
import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap' import { MetadataController } from '../controllers'
import { MetadataController, ZapController } from '../controllers' import { useAppDispatch, useAppSelector, useDidMount } from '../hooks'
import { useAppDispatch, useAppSelector } from '../hooks'
import { appRoutes } from '../routes' import { appRoutes } from '../routes'
import { setAuth, setUser } from '../store/reducers/user' import { setAuth, setUser } from '../store/reducers/user'
import mainStyles from '../styles//main.module.scss' import mainStyles from '../styles//main.module.scss'
import navStyles from '../styles/nav.module.scss' import navStyles from '../styles/nav.module.scss'
import '../styles/popup.css' import '../styles/popup.css'
import { PaymentRequest } from '../types' import { npubToHex } from '../utils'
import { formatNumber, npubToHex, unformatNumber } from '../utils'
export const Header = () => { export const Header = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -173,7 +170,9 @@ export const Header = () => {
<div className={navStyles.NavMainBottom}> <div className={navStyles.NavMainBottom}>
<div className={mainStyles.ContainerMain}> <div className={mainStyles.ContainerMain}>
<div className={navStyles.NavMainBottomInside}> <div className={navStyles.NavMainBottomInside}>
<div className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherLeft}`}></div> <div
className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherLeft}`}
></div>
<div className={navStyles.NavMainBottomInsideLinks}> <div className={navStyles.NavMainBottomInsideLinks}>
<Link <Link
to={appRoutes.games} to={appRoutes.games}
@ -200,31 +199,48 @@ export const Header = () => {
Blog Blog
</Link> </Link>
</div> </div>
<div className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherRight}`}> <div
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://primal.net/p/npub17jl3ldd6305rnacvwvchx03snauqsg4nz8mruq0emj9thdpglr2sst825x" target="_blank"> className={`${navStyles.NavMainBottomInsideOther} ${navStyles.NavMainBottomInsideOtherRight}`}
<img src="https://image.nostr.build/fb557f1b6d58c7bbcdf4d1edb1b48090c76ff1d1384b9d1aae13d652e7a3cfe4.gif" width="15px" /> >
<a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://primal.net/p/npub17jl3ldd6305rnacvwvchx03snauqsg4nz8mruq0emj9thdpglr2sst825x'
target='_blank'
>
<img
src='https://image.nostr.build/fb557f1b6d58c7bbcdf4d1edb1b48090c76ff1d1384b9d1aae13d652e7a3cfe4.gif'
width='15px'
/>
</a> </a>
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://x.com/DEGMods" target="_blank"> <a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://x.com/DEGMods'
target='_blank'
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512' viewBox='0 0 512 512'
width='1em' width='1em'
height='1em' height='1em'
fill='currentColor' fill='currentColor'
> >
<path d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'></path> <path d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'></path>
</svg> </svg>
</a> </a>
<a className={navStyles.NavMainBottomInsideOtherLink} href="https://www.youtube.com/@DEGModsDotCom" target="_blank"> <a
className={navStyles.NavMainBottomInsideOtherLink}
href='https://www.youtube.com/@DEGModsDotCom'
target='_blank'
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512' viewBox='0 0 512 512'
width='1em' width='1em'
height='1em' height='1em'
fill='currentColor' fill='currentColor'
> >
<path d='M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z'></path> <path d='M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z'></path>
</svg> </svg>
</a> </a>
</div> </div>
</div> </div>
@ -235,124 +251,13 @@ export const Header = () => {
} }
const TipButtonWithDialog = React.memo(() => { const TipButtonWithDialog = React.memo(() => {
const [adminNpub, setAdminNpub] = useState<string | null>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false) useDidMount(async () => {
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const metadataController = await MetadataController.getInstance()
setAdminNpub(metadataController.adminNpubs[0])
const [amount, setAmount] = useState<number>(0) })
const [message, setMessage] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
setIsOpen(false)
}, [])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | 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
}
setLoadingSpinnerDesc('Getting admin metadata')
const metadataController = await MetadataController.getInstance()
const adminMetadata = await metadataController.findAdminMetadata()
if (!adminMetadata?.lud16) {
setIsLoading(false)
toast.error('Lighting address (lud16) is missing in admin metadata!')
return null
}
if (!adminMetadata?.pubkey) {
setIsLoading(false)
toast.error('pubkey is missing in admin metadata!')
return null
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
adminMetadata.lud16,
amount,
adminMetadata.pubkey as string,
userHexKey,
message
)
.catch((err) => {
toast.error(err.message || err)
return null
})
.finally(() => {
setIsLoading(false)
})
}, [amount, message, userState])
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!`)
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, handleClose, generatePaymentRequest])
const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setPaymentRequest(pr)
}
return ( return (
<> <>
@ -371,91 +276,38 @@ const TipButtonWithDialog = React.memo(() => {
</svg> </svg>
Tip Tip
</a> </a>
{isOpen && ( {isOpen && adminNpub && (
<div id='PopUpMainZap' className='popUpMain'> <ZapPopUp
<div className='ContainerMain'> title='Tip/Zap DEG Mods'
<div className='popUpMainCardWrapper'> receiver={adminNpub}
<div className='popUpMainCard popUpMainCardQR'> handleClose={() => setIsOpen(false)}
<div className='popUpMainCardTop'> labelDescriptionMain={
<div className='popUpMainCardTopInfo'> <p className='labelDescriptionMain' style={{ textAlign: 'center' }}>
<h3>Tip/Zap DEG Mods</h3> If you don't want the development and maintenance of DEG Mods to
</div> stop, then a tip helps!
<div className='popUpMainCardTopClose' onClick={handleClose}> </p>
<svg }
xmlns='http://www.w3.org/2000/svg' lastNode={
viewBox='-96 0 512 512' <div className='BTCAddressPopZap'>
width='1em' <p>
height='1em' DEG Mod's Silent Payment Bitcoin Address (Be careful.{' '}
fill='currentColor' <a
style={{ zIndex: 1 }} href='https://youtu.be/payDPlHzp58?t=215'
> className='linkMain'
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z' /> target='_blank'
</svg> >
</div> Learn more
</div> </a>
<div className='pUMCB_Zaps'> ):
<div className='pUMCB_ZapsInside'> <br />
<div className='pUMCB_ZapsInsideAmount'> <span className='BTCAddressPopZapTextSpan'>
<div className='inputLabelWrapperMain'> sp1qq205tj23sq3z6qjxt5ts5ps8gdwcrkwypej3h2z2hdclmaptl25xxqjfqhc2de4gaxprgm0yqwfr737swpvvmrph9ctkeyk60knz6xpjhqumafrd
<p </span>
className='labelDescriptionMain' </p>
style={{ textAlign: 'center' }}
>
If you don't want the development and maintenance of DEG
Mods to stop, then a tip helps!
</p>
<label className='form-label labelMain'>
Amount (Satoshis)
</label>
<input
className='inputMain'
type='text'
inputMode='numeric'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>
Message (optional)
</label>
<input
type='text'
className='inputMain'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleClose}
handleQRExpiry={handleQRExpiry}
/>
)}
<div className='BTCAddressPopZap'>
<p>
DEG Mod's Silent Payment Bitcoin Address (Be careful. <a href='https://youtu.be/payDPlHzp58?t=215' className='linkMain' target='_blank'>Learn more</a>):<br />
<span className='BTCAddressPopZapTextSpan'>sp1qq205tj23sq3z6qjxt5ts5ps8gdwcrkwypej3h2z2hdclmaptl25xxqjfqhc2de4gaxprgm0yqwfr737swpvvmrph9ctkeyk60knz6xpjhqumafrd</span>
</p>
</div>
</div>
</div>
</div>
</div> </div>
</div> }
</div> />
)} )}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</> </>
) )
}) })

View File

@ -2,21 +2,15 @@ import Link from '@tiptap/extension-link'
import { EditorContent, useEditor } from '@tiptap/react' import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { formatDate } from 'date-fns' import { formatDate } from 'date-fns'
import FsLightbox from 'fslightbox-react'
import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import { import { useCallback, useEffect, useRef, useState } from 'react'
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
useState
} from 'react'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { BlogCard } from '../components/BlogCard' import { BlogCard } from '../components/BlogCard'
import { LoadingSpinner } from '../components/LoadingSpinner' import { LoadingSpinner } from '../components/LoadingSpinner'
import { ProfileSection } from '../components/ProfileSection' import { ProfileSection } from '../components/ProfileSection'
import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap' import { ZapButtons, ZapPopUp, ZapPresets, ZapQR } from '../components/Zap'
import { import {
MetadataController, MetadataController,
RelayController, RelayController,
@ -48,10 +42,9 @@ import {
now, now,
npubToHex, npubToHex,
sendDMUsingRandomKey, sendDMUsingRandomKey,
unformatNumber, signAndPublish,
signAndPublish unformatNumber
} from '../utils' } from '../utils'
import FsLightbox from 'fslightbox-react'
export const ModPage = () => { export const ModPage = () => {
const { naddr } = useParams() const { naddr } = useParams()
@ -1638,221 +1631,17 @@ const Zap = ({ modDetails }: ZapProps) => {
<div className='IBMSMSMBSSCL_CAElementLoad'></div> <div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div> </div>
</div> </div>
{isOpen && <ZapModal modDetails={modDetails} handleClose={setIsOpen} />} {isOpen && (
</> <ZapPopUp
) title='Tip/Zap'
} receiver={modDetails.author}
eventId={modDetails.id}
type ZapModalProps = { aTag={modDetails.aTag}
modDetails: ModDetails handleClose={() => setIsOpen(false)}
handleClose: Dispatch<SetStateAction<boolean>> lastNode={<ZapSite />}
} notCloseAfterZap
const ZapModal = ({ modDetails, handleClose }: ZapModalProps) => {
return (
<div id='PopUpMainZapSplitAlt' className='popUpMain'>
<div className='ContainerMain'>
<div className='popUpMainCardWrapper'>
<div className='popUpMainCard popUpMainCardQR'>
<div className='popUpMainCardTop'>
<div className='popUpMainCardTopInfo'>
<h3>Tip/Zap</h3>
</div>
<div
className='popUpMainCardTopClose'
onClick={() => handleClose(false)}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
style={{ zIndex: 1 }}
>
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
</svg>
</div>
</div>
<div className='pUMCB_Zaps'>
<div className='pUMCB_ZapsInside'>
<ZapMod modDetails={modDetails} />
<ZapSite />
</div>
</div>
</div>
</div>
</div>
</div>
)
}
type ZapModProps = {
modDetails: ModDetails
}
const ZapMod = ({ modDetails }: ZapModProps) => {
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [amount, setAmount] = useState(0)
const [message, setMessage] = useState('')
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
const userState = useAppSelector((state) => state.user)
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const unformattedValue = unformatNumber(event.target.value)
setAmount(unformattedValue)
}
const handleClose = useCallback(() => {
setPaymentRequest(undefined)
setIsLoading(false)
}, [])
const handleQRExpiry = useCallback(() => {
setPaymentRequest(undefined)
}, [])
const generatePaymentRequest =
useCallback(async (): Promise<PaymentRequest | 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
}
setLoadingSpinnerDesc('Getting admin metadata')
const metadataController = await MetadataController.getInstance()
const authorMetadata = await metadataController.findMetadata(
modDetails.author
)
if (!authorMetadata?.lud16) {
setIsLoading(false)
toast.error('Lighting address (lud16) is missing in author metadata!')
return null
}
if (!authorMetadata?.pubkey) {
setIsLoading(false)
toast.error('pubkey is missing in author metadata!')
return null
}
const zapController = ZapController.getInstance()
setLoadingSpinnerDesc('Creating zap request')
return await zapController
.getLightningPaymentRequest(
authorMetadata.lud16,
amount,
authorMetadata.pubkey as string,
userHexKey,
message,
modDetails.id,
modDetails.aTag
)
.catch((err) => {
toast.error(err.message || err)
return null
})
.finally(() => {
setIsLoading(false)
})
}, [amount, message, userState, modDetails])
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!`)
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, handleClose, generatePaymentRequest])
const handleGenerateQRCode = async () => {
const pr = await generatePaymentRequest()
if (!pr) return
setPaymentRequest(pr)
}
return (
<>
<div className='pUMCB_ZapsInsideAmount'>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Amount (Satoshis)</label>
<input
type='text'
className='inputMain'
inputMode='numeric'
placeholder='69 or 420? or 69,420?'
value={amount ? formatNumber(amount) : ''}
onChange={handleAmountChange}
/>
</div>
<div className='pUMCB_ZapsInsideAmountOptions'>
<ZapPresets setAmount={setAmount} />
</div>
</div>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Message (optional)</label>
<input
type='text'
className='inputMain'
placeholder='This is awesome!'
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<ZapButtons
disabled={!amount}
handleGenerateQRCode={handleGenerateQRCode}
handleSend={handleSend}
/>
{paymentRequest && (
<ZapQR
paymentRequest={paymentRequest}
handleClose={handleClose}
handleQRExpiry={handleQRExpiry}
/> />
)} )}
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
</> </>
) )
} }