feat: implemented the logic for zapping mod
All checks were successful
Release to Staging / build_and_release (push) Successful in 43s
All checks were successful
Release to Staging / build_and_release (push) Successful in 43s
This commit is contained in:
parent
42c40c2d8e
commit
a8a2d3dbf3
@ -4,6 +4,12 @@ import { MuteLists } from '../types'
|
||||
import { UserProfile } from '../types/user'
|
||||
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
||||
|
||||
export enum UserRelaysType {
|
||||
Read = 'readRelayUrls',
|
||||
Write = 'writeRelayUrls',
|
||||
Both = 'bothRelayUrls'
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton class to manage metadata operations using NDK.
|
||||
*/
|
||||
@ -110,14 +116,17 @@ export class MetadataController {
|
||||
return this.findMetadata(this.adminNpubs[0])
|
||||
}
|
||||
|
||||
public findWriteRelays = async (hexKey: string) => {
|
||||
public findUserRelays = async (
|
||||
hexKey: string,
|
||||
userRelaysType: UserRelaysType = UserRelaysType.Both
|
||||
) => {
|
||||
const ndkRelayList = await getRelayListForUser(hexKey, this.ndk)
|
||||
|
||||
if (!ndkRelayList) {
|
||||
throw new Error(`Couldn't found user's relay list`)
|
||||
}
|
||||
|
||||
return ndkRelayList.writeRelayUrls
|
||||
return ndkRelayList[userRelaysType]
|
||||
}
|
||||
|
||||
public getAdminsMuteLists = async (): Promise<MuteLists> => {
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { Event, Filter, Relay } from 'nostr-tools'
|
||||
import { log, LogType, normalizeWebSocketURL, timeout } from '../utils'
|
||||
import { MetadataController } from './metadata'
|
||||
import { Event, Filter, kinds, Relay } from 'nostr-tools'
|
||||
import {
|
||||
extractZapAmount,
|
||||
log,
|
||||
LogType,
|
||||
normalizeWebSocketURL,
|
||||
timeout
|
||||
} from '../utils'
|
||||
import { MetadataController, UserRelaysType } from './metadata'
|
||||
import { ModDetails } from '../types'
|
||||
|
||||
/**
|
||||
* Singleton class to manage relay operations.
|
||||
@ -72,7 +79,10 @@ export class RelayController {
|
||||
|
||||
// Retrieve the list of write relays for the event's public key
|
||||
// Use a timeout to handle cases where retrieving write relays takes too long
|
||||
const writeRelaysPromise = metadataController.findWriteRelays(event.pubkey)
|
||||
const writeRelaysPromise = metadataController.findUserRelays(
|
||||
event.pubkey,
|
||||
UserRelaysType.Write
|
||||
)
|
||||
|
||||
log(this.debug, LogType.Info, `ℹ Finding user's write relays`)
|
||||
|
||||
@ -236,4 +246,93 @@ export class RelayController {
|
||||
// Return the most recent event, or null if no events were received
|
||||
return events[0] || null
|
||||
}
|
||||
|
||||
getTotalZapAmount = async (
|
||||
modDetails: ModDetails,
|
||||
currentLoggedInUser?: string
|
||||
) => {
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
|
||||
const authorReadRelaysPromise = metadataController.findUserRelays(
|
||||
modDetails.author,
|
||||
UserRelaysType.Read
|
||||
)
|
||||
|
||||
log(this.debug, LogType.Info, `ℹ Finding user's read relays`)
|
||||
|
||||
// Use Promise.race to either get the write relay URLs or timeout
|
||||
const relayUrls = await Promise.race([
|
||||
authorReadRelaysPromise,
|
||||
timeout() // This is a custom timeout function that rejects the promise after a specified time
|
||||
]).catch((err) => {
|
||||
log(this.debug, LogType.Error, err)
|
||||
return [] as string[] // Return an empty array if an error occurs
|
||||
})
|
||||
|
||||
// add app relay to relays array
|
||||
relayUrls.push(import.meta.env.VITE_APP_RELAY)
|
||||
|
||||
// add admin relays to relays array
|
||||
metadataController.adminRelays.forEach((url) => {
|
||||
relayUrls.push(url)
|
||||
})
|
||||
|
||||
// Connect to all specified relays
|
||||
const relayPromises = relayUrls.map((relayUrl) =>
|
||||
this.connectRelay(relayUrl)
|
||||
)
|
||||
await Promise.allSettled(relayPromises)
|
||||
|
||||
let accumulatedZapAmount = 0
|
||||
let hasZapped = false
|
||||
|
||||
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
|
||||
|
||||
// Create a promise for each relay subscription
|
||||
const subPromises = this.connectedRelays.map((relay) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
// Subscribe to the relay with the specified filter
|
||||
const sub = relay.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [kinds.Zap],
|
||||
'#a': [modDetails.aTag]
|
||||
}
|
||||
],
|
||||
{
|
||||
// Handle incoming events
|
||||
onevent: (e) => {
|
||||
// Add the event to the array if it's not a duplicate
|
||||
if (!eventIds.has(e.id)) {
|
||||
console.log('e :>> ', e)
|
||||
eventIds.add(e.id) // Record the event ID
|
||||
const amount = extractZapAmount(e)
|
||||
accumulatedZapAmount += amount
|
||||
|
||||
if (!hasZapped) {
|
||||
hasZapped =
|
||||
e.tags.findIndex(
|
||||
(tag) => tag[0] === 'P' && tag[1] === currentLoggedInUser
|
||||
) > -1
|
||||
}
|
||||
}
|
||||
},
|
||||
// Handle the End-Of-Stream (EOSE) message
|
||||
oneose: () => {
|
||||
sub.close() // Close the subscription
|
||||
resolve() // Resolve the promise when EOSE is received
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Wait for all subscriptions to complete
|
||||
await Promise.allSettled(subPromises)
|
||||
|
||||
return {
|
||||
accumulatedZapAmount,
|
||||
hasZapped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ export class ZapController {
|
||||
* @param senderPubkey - pubKey of of the sender.
|
||||
* @param content - optional content (comment).
|
||||
* @param eventId - event id, if zapping an event.
|
||||
* @param aTag - value of `a` tag.
|
||||
* @returns - promise that resolves into object containing zap request and payment
|
||||
* request string
|
||||
*/
|
||||
@ -56,7 +57,8 @@ export class ZapController {
|
||||
recipientPubKey: string,
|
||||
senderPubkey: string,
|
||||
content?: string,
|
||||
eventId?: string
|
||||
eventId?: string,
|
||||
aTag?: string
|
||||
) {
|
||||
// Check if amount is greater than 0
|
||||
if (amount <= 0) throw 'Amount should be > 0.'
|
||||
@ -90,7 +92,8 @@ export class ZapController {
|
||||
lnurlBech32,
|
||||
recipientPubKey,
|
||||
senderPubkey,
|
||||
eventId
|
||||
eventId,
|
||||
aTag
|
||||
)
|
||||
|
||||
if (!window.nostr?.signEvent) {
|
||||
@ -272,6 +275,7 @@ export class ZapController {
|
||||
* @param recipientPubKey - pubKey of the recipient.
|
||||
* @param senderPubkey - pubKey of of the sender.
|
||||
* @param eventId - event id, if zapping an event.
|
||||
* @param aTag - value of `a` tag.
|
||||
* @returns zap request
|
||||
*/
|
||||
private async createZapRequest(
|
||||
@ -280,7 +284,8 @@ export class ZapController {
|
||||
lnurl: string,
|
||||
recipientPubKey: string,
|
||||
senderPubkey: string,
|
||||
eventId?: string
|
||||
eventId?: string,
|
||||
aTag?: string
|
||||
): Promise<ZapRequest> {
|
||||
const recipientHexKey = npubToHex(recipientPubKey)
|
||||
|
||||
@ -302,6 +307,8 @@ export class ZapController {
|
||||
// add event id to the tags, if zapping an event.
|
||||
if (eventId) zapRequest.tags.push(['e', eventId])
|
||||
|
||||
if (aTag) zapRequest.tags.push(['a', aTag])
|
||||
|
||||
return zapRequest
|
||||
}
|
||||
|
||||
|
@ -334,11 +334,7 @@ const TipButtonWithDialog = React.memo(() => {
|
||||
Tip
|
||||
</a>
|
||||
{isOpen && (
|
||||
<div
|
||||
id='PopUpMainZap'
|
||||
className='popUpMain'
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
<div id='PopUpMainZap' className='popUpMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='popUpMainCardWrapper'>
|
||||
<div className='popUpMainCard popUpMainCardQR'>
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { formatDate } from 'date-fns'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { Filter, nip19 } from 'nostr-tools'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { BlogCard } from '../components/BlogCard'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||
import { ProfileSection } from '../components/ProfileSection'
|
||||
import { RelayController } from '../controllers'
|
||||
import {
|
||||
MetadataController,
|
||||
RelayController,
|
||||
ZapController
|
||||
} from '../controllers'
|
||||
import { useAppSelector, useDidMount } from '../hooks'
|
||||
import '../styles/comments.css'
|
||||
import '../styles/downloads.css'
|
||||
@ -18,16 +22,21 @@ import '../styles/styles.css'
|
||||
import '../styles/tabs.css'
|
||||
import '../styles/tags.css'
|
||||
import '../styles/write.css'
|
||||
import { ModDetails } from '../types'
|
||||
import '../styles/popup.css'
|
||||
import { ModDetails, PaymentRequest } from '../types'
|
||||
import {
|
||||
abbreviateNumber,
|
||||
copyTextToClipboard,
|
||||
extractModData,
|
||||
formatNumber,
|
||||
getFilenameFromUrl,
|
||||
log,
|
||||
LogType
|
||||
LogType,
|
||||
unformatNumber
|
||||
} from '../utils'
|
||||
|
||||
import saveAs from 'file-saver'
|
||||
import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap'
|
||||
|
||||
export const InnerModPage = () => {
|
||||
const { nevent } = useParams()
|
||||
@ -113,7 +122,7 @@ export const InnerModPage = () => {
|
||||
tags={modData.tags}
|
||||
nsfw={modData.nsfw}
|
||||
/>
|
||||
<Interactions />
|
||||
<Interactions modDetails={modData} />
|
||||
<PublishDetails
|
||||
published_at={modData.published_at}
|
||||
edited_at={modData.edited_at}
|
||||
@ -396,7 +405,11 @@ const Body = ({
|
||||
)
|
||||
}
|
||||
|
||||
const Interactions = () => {
|
||||
type InteractionsProps = {
|
||||
modDetails: ModDetails
|
||||
}
|
||||
|
||||
const Interactions = ({ modDetails }: InteractionsProps) => {
|
||||
return (
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
<div className='IBMSMSMBSS_Details'>
|
||||
@ -420,27 +433,7 @@ const Interactions = () => {
|
||||
<p className='IBMSMSMBSS_Details_CardText'>420</p>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
id='reactBolt'
|
||||
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CBolt'
|
||||
>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSS_Details_CardVisualIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>69k</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
<Zap modDetails={modDetails} />
|
||||
<div
|
||||
id='reactUp'
|
||||
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp IBMSMSMBSS_D_CRUActive'
|
||||
@ -924,3 +917,446 @@ const Comments = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ZapProps = {
|
||||
modDetails: ModDetails
|
||||
}
|
||||
|
||||
const Zap = ({ modDetails }: ZapProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [hasZapped, setHasZapped] = useState(false)
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const [totalZappedAmount, setTotalZappedAmount] = useState('0')
|
||||
|
||||
useDidMount(() => {
|
||||
RelayController.getInstance()
|
||||
.getTotalZapAmount(modDetails, userState.user?.pubkey as string)
|
||||
.then((res) => {
|
||||
setTotalZappedAmount(abbreviateNumber(res.accumulatedZapAmount))
|
||||
setHasZapped(res.hasZapped)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id='reactBolt'
|
||||
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CBolt ${
|
||||
hasZapped ? 'IBMSMSMBSS_D_CBActive' : ''
|
||||
}`}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<div className='IBMSMSMBSS_Details_CardVisual'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-64 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMBSS_Details_CardVisualIcon'
|
||||
>
|
||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className='IBMSMSMBSS_Details_CardText'>{totalZappedAmount}</p>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && <ZapModal modDetails={modDetails} handleClose={setIsOpen} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type ZapModalProps = {
|
||||
modDetails: ModDetails
|
||||
handleClose: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
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.isAuth && 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} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ZapSite = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const [amount, setAmount] = useState(0)
|
||||
|
||||
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.isAuth && 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
|
||||
)
|
||||
.catch((err) => {
|
||||
toast.error(err.message || err)
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [amount, 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 (
|
||||
<>
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>
|
||||
Tip DEG Mods too (Optional)
|
||||
</label>
|
||||
<div className='ZapSplitUserBox'>
|
||||
<div className='ZapSplitUserBoxUser'>
|
||||
<div
|
||||
className='ZapSplitUserBoxUserPic'
|
||||
style={{
|
||||
background: `url('/assets/img/Logo%20with%20circle.png')
|
||||
center / cover no-repeat`
|
||||
}}
|
||||
></div>
|
||||
<div className='ZapSplitUserBoxUserDetails'>
|
||||
<p className='ZapSplitUserBoxUserDetailsName'>DEG Mods</p>
|
||||
<p className='ZapSplitUserBoxUserDetailsHandle'>
|
||||
degmods@degmods.com
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className='ZapSplitUserBoxText'>
|
||||
Help with the development, maintenance, management, and growth of
|
||||
DEG Mods.
|
||||
</p>
|
||||
<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>
|
||||
<ZapButtons
|
||||
disabled={!amount}
|
||||
handleGenerateQRCode={handleGenerateQRCode}
|
||||
handleSend={handleSend}
|
||||
/>
|
||||
{paymentRequest && (
|
||||
<ZapQR
|
||||
paymentRequest={paymentRequest}
|
||||
handleClose={handleClose}
|
||||
handleQRExpiry={handleQRExpiry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -84,3 +84,40 @@ export const npubToHex = (pubKey: string): string | null => {
|
||||
// Not a valid hex key
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the zap amount from an event object.
|
||||
*
|
||||
* @param event - The event object from which the zap amount needs to be extracted.
|
||||
* @returns The zap amount in the form of a number, converted from the extracted data, or 0 if the amount cannot be determined.
|
||||
*/
|
||||
export const extractZapAmount = (event: Event): number => {
|
||||
// Find the 'description' tag within the event's tags
|
||||
const description = event.tags.find(
|
||||
(tag) => tag[0] === 'description' && typeof tag[1] === 'string'
|
||||
)
|
||||
|
||||
// If the 'description' tag is found and it has a valid value
|
||||
if (description && description[1]) {
|
||||
try {
|
||||
// Parse the description as JSON to get additional details
|
||||
const parsedDescription: Event = JSON.parse(description[1])
|
||||
|
||||
// Find the 'amount' tag within the parsed description's tags
|
||||
const amountTag = parsedDescription.tags.find(
|
||||
(tag) => tag[0] === 'amount' && typeof tag[1] === 'string'
|
||||
)
|
||||
|
||||
// If the 'amount' tag is found and it has a valid value, convert it to an integer and return
|
||||
if (amountTag && amountTag[1]) return parseInt(amountTag[1]) / 1000
|
||||
} catch (error) {
|
||||
// Log an error message if JSON parsing fails
|
||||
console.log(
|
||||
`An error occurred while parsing description of zap event: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Return 0 if the zap amount cannot be determined
|
||||
return 0
|
||||
}
|
||||
|
@ -97,3 +97,29 @@ export const unformatNumber = (value: string): number => {
|
||||
// If `parseFloat` fails to parse the string, `|| 0` ensures that the function returns 0.
|
||||
return parseFloat(value.replace(/,/g, '')) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number into a more readable string with suffixes.
|
||||
*
|
||||
* @param value - The number to be formatted.
|
||||
* @returns A string representing the formatted number with suffixes.
|
||||
* - "K" for thousands
|
||||
* - "M" for millions
|
||||
* - "B" for billions
|
||||
* - The number as-is if it's less than a thousand
|
||||
*/
|
||||
export const abbreviateNumber = (value: number): string => {
|
||||
if (value >= 1000000000) {
|
||||
// Format as billions
|
||||
return `${(value / 1000000000).toFixed(1)}B`
|
||||
} else if (value >= 1000000) {
|
||||
// Format as millions
|
||||
return `${(value / 1000000).toFixed(1)}M`
|
||||
} else if (value >= 1000) {
|
||||
// Format as thousands
|
||||
return `${(value / 1000).toFixed(1)}K`
|
||||
} else {
|
||||
// Format as regular number
|
||||
return value.toString()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user