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 { UserProfile } from '../types/user'
|
||||||
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
||||||
|
|
||||||
|
export enum UserRelaysType {
|
||||||
|
Read = 'readRelayUrls',
|
||||||
|
Write = 'writeRelayUrls',
|
||||||
|
Both = 'bothRelayUrls'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton class to manage metadata operations using NDK.
|
* Singleton class to manage metadata operations using NDK.
|
||||||
*/
|
*/
|
||||||
@ -110,14 +116,17 @@ export class MetadataController {
|
|||||||
return this.findMetadata(this.adminNpubs[0])
|
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)
|
const ndkRelayList = await getRelayListForUser(hexKey, this.ndk)
|
||||||
|
|
||||||
if (!ndkRelayList) {
|
if (!ndkRelayList) {
|
||||||
throw new Error(`Couldn't found user's relay list`)
|
throw new Error(`Couldn't found user's relay list`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ndkRelayList.writeRelayUrls
|
return ndkRelayList[userRelaysType]
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAdminsMuteLists = async (): Promise<MuteLists> => {
|
public getAdminsMuteLists = async (): Promise<MuteLists> => {
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { Event, Filter, Relay } from 'nostr-tools'
|
import { Event, Filter, kinds, Relay } from 'nostr-tools'
|
||||||
import { log, LogType, normalizeWebSocketURL, timeout } from '../utils'
|
import {
|
||||||
import { MetadataController } from './metadata'
|
extractZapAmount,
|
||||||
|
log,
|
||||||
|
LogType,
|
||||||
|
normalizeWebSocketURL,
|
||||||
|
timeout
|
||||||
|
} from '../utils'
|
||||||
|
import { MetadataController, UserRelaysType } from './metadata'
|
||||||
|
import { ModDetails } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton class to manage relay operations.
|
* 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
|
// 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
|
// 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`)
|
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 the most recent event, or null if no events were received
|
||||||
return events[0] || null
|
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 senderPubkey - pubKey of of the sender.
|
||||||
* @param content - optional content (comment).
|
* @param content - optional content (comment).
|
||||||
* @param eventId - event id, if zapping an event.
|
* @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
|
* @returns - promise that resolves into object containing zap request and payment
|
||||||
* request string
|
* request string
|
||||||
*/
|
*/
|
||||||
@ -56,7 +57,8 @@ export class ZapController {
|
|||||||
recipientPubKey: string,
|
recipientPubKey: string,
|
||||||
senderPubkey: string,
|
senderPubkey: string,
|
||||||
content?: string,
|
content?: string,
|
||||||
eventId?: string
|
eventId?: string,
|
||||||
|
aTag?: string
|
||||||
) {
|
) {
|
||||||
// Check if amount is greater than 0
|
// Check if amount is greater than 0
|
||||||
if (amount <= 0) throw 'Amount should be > 0.'
|
if (amount <= 0) throw 'Amount should be > 0.'
|
||||||
@ -90,7 +92,8 @@ export class ZapController {
|
|||||||
lnurlBech32,
|
lnurlBech32,
|
||||||
recipientPubKey,
|
recipientPubKey,
|
||||||
senderPubkey,
|
senderPubkey,
|
||||||
eventId
|
eventId,
|
||||||
|
aTag
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!window.nostr?.signEvent) {
|
if (!window.nostr?.signEvent) {
|
||||||
@ -272,6 +275,7 @@ export class ZapController {
|
|||||||
* @param recipientPubKey - pubKey of the recipient.
|
* @param recipientPubKey - pubKey of the recipient.
|
||||||
* @param senderPubkey - pubKey of of the sender.
|
* @param senderPubkey - pubKey of of the sender.
|
||||||
* @param eventId - event id, if zapping an event.
|
* @param eventId - event id, if zapping an event.
|
||||||
|
* @param aTag - value of `a` tag.
|
||||||
* @returns zap request
|
* @returns zap request
|
||||||
*/
|
*/
|
||||||
private async createZapRequest(
|
private async createZapRequest(
|
||||||
@ -280,7 +284,8 @@ export class ZapController {
|
|||||||
lnurl: string,
|
lnurl: string,
|
||||||
recipientPubKey: string,
|
recipientPubKey: string,
|
||||||
senderPubkey: string,
|
senderPubkey: string,
|
||||||
eventId?: string
|
eventId?: string,
|
||||||
|
aTag?: string
|
||||||
): Promise<ZapRequest> {
|
): Promise<ZapRequest> {
|
||||||
const recipientHexKey = npubToHex(recipientPubKey)
|
const recipientHexKey = npubToHex(recipientPubKey)
|
||||||
|
|
||||||
@ -302,6 +307,8 @@ export class ZapController {
|
|||||||
// add event id to the tags, if zapping an event.
|
// add event id to the tags, if zapping an event.
|
||||||
if (eventId) zapRequest.tags.push(['e', eventId])
|
if (eventId) zapRequest.tags.push(['e', eventId])
|
||||||
|
|
||||||
|
if (aTag) zapRequest.tags.push(['a', aTag])
|
||||||
|
|
||||||
return zapRequest
|
return zapRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,11 +334,7 @@ const TipButtonWithDialog = React.memo(() => {
|
|||||||
Tip
|
Tip
|
||||||
</a>
|
</a>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div id='PopUpMainZap' className='popUpMain'>
|
||||||
id='PopUpMainZap'
|
|
||||||
className='popUpMain'
|
|
||||||
style={{ display: 'flex' }}
|
|
||||||
>
|
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
<div className='popUpMainCardWrapper'>
|
<div className='popUpMainCardWrapper'>
|
||||||
<div className='popUpMainCard popUpMainCardQR'>
|
<div className='popUpMainCard popUpMainCardQR'>
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { formatDate } from 'date-fns'
|
import { formatDate } from 'date-fns'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import { Filter, nip19 } from 'nostr-tools'
|
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 { 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 { RelayController } from '../controllers'
|
import {
|
||||||
|
MetadataController,
|
||||||
|
RelayController,
|
||||||
|
ZapController
|
||||||
|
} from '../controllers'
|
||||||
import { useAppSelector, useDidMount } from '../hooks'
|
import { useAppSelector, useDidMount } from '../hooks'
|
||||||
import '../styles/comments.css'
|
import '../styles/comments.css'
|
||||||
import '../styles/downloads.css'
|
import '../styles/downloads.css'
|
||||||
@ -18,16 +22,21 @@ import '../styles/styles.css'
|
|||||||
import '../styles/tabs.css'
|
import '../styles/tabs.css'
|
||||||
import '../styles/tags.css'
|
import '../styles/tags.css'
|
||||||
import '../styles/write.css'
|
import '../styles/write.css'
|
||||||
import { ModDetails } from '../types'
|
import '../styles/popup.css'
|
||||||
|
import { ModDetails, PaymentRequest } from '../types'
|
||||||
import {
|
import {
|
||||||
|
abbreviateNumber,
|
||||||
copyTextToClipboard,
|
copyTextToClipboard,
|
||||||
extractModData,
|
extractModData,
|
||||||
|
formatNumber,
|
||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
log,
|
log,
|
||||||
LogType
|
LogType,
|
||||||
|
unformatNumber
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
|
import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap'
|
||||||
|
|
||||||
export const InnerModPage = () => {
|
export const InnerModPage = () => {
|
||||||
const { nevent } = useParams()
|
const { nevent } = useParams()
|
||||||
@ -113,7 +122,7 @@ export const InnerModPage = () => {
|
|||||||
tags={modData.tags}
|
tags={modData.tags}
|
||||||
nsfw={modData.nsfw}
|
nsfw={modData.nsfw}
|
||||||
/>
|
/>
|
||||||
<Interactions />
|
<Interactions modDetails={modData} />
|
||||||
<PublishDetails
|
<PublishDetails
|
||||||
published_at={modData.published_at}
|
published_at={modData.published_at}
|
||||||
edited_at={modData.edited_at}
|
edited_at={modData.edited_at}
|
||||||
@ -396,7 +405,11 @@ const Body = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Interactions = () => {
|
type InteractionsProps = {
|
||||||
|
modDetails: ModDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
const Interactions = ({ modDetails }: InteractionsProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='IBMSMSplitMainBigSideSec'>
|
<div className='IBMSMSplitMainBigSideSec'>
|
||||||
<div className='IBMSMSMBSS_Details'>
|
<div className='IBMSMSMBSS_Details'>
|
||||||
@ -420,27 +433,7 @@ const Interactions = () => {
|
|||||||
<p className='IBMSMSMBSS_Details_CardText'>420</p>
|
<p className='IBMSMSMBSS_Details_CardText'>420</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div
|
<Zap modDetails={modDetails} />
|
||||||
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>
|
|
||||||
<div
|
<div
|
||||||
id='reactUp'
|
id='reactUp'
|
||||||
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp IBMSMSMBSS_D_CRUActive'
|
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp IBMSMSMBSS_D_CRUActive'
|
||||||
@ -924,3 +917,446 @@ const Comments = () => {
|
|||||||
</div>
|
</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
|
// Not a valid hex key
|
||||||
return null
|
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.
|
// If `parseFloat` fails to parse the string, `|| 0` ensures that the function returns 0.
|
||||||
return parseFloat(value.replace(/,/g, '')) || 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