diff --git a/src/components/Zap.tsx b/src/components/Zap.tsx index b78cbe7..1c87ca8 100644 --- a/src/components/Zap.tsx +++ b/src/components/Zap.tsx @@ -12,14 +12,16 @@ import { toast } from 'react-toastify' import { MetadataController, ZapController } from '../controllers' import { useAppSelector, useDidMount } from '../hooks' import '../styles/popup.css' -import { PaymentRequest } from '../types' +import { PaymentRequest, UserProfile } from '../types' import { copyTextToClipboard, formatNumber, + getTagValue, getZapAmount, unformatNumber } from '../utils' import { LoadingSpinner } from './LoadingSpinner' +import { FALLBACK_PROFILE_IMAGE } from 'constants.ts' type PresetAmountProps = { label: string @@ -460,3 +462,405 @@ export const ZapPopUp = ({ ) } + +type ZapSplitProps = { + pubkey: string + eventId?: string + aTag?: string + setTotalZapAmount?: Dispatch> + setHasZapped?: Dispatch> + handleClose: () => void +} + +export const ZapSplit = ({ + pubkey, + eventId, + aTag, + setTotalZapAmount, + setHasZapped, + handleClose +}: ZapSplitProps) => { + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + const [amount, setAmount] = useState(0) + const [message, setMessage] = useState('') + const [authorPercentage, setAuthorPercentage] = useState(95) + const [adminPercentage, setAdminPercentage] = useState(5) + + const [author, setAuthor] = useState() + const [admin, setAdmin] = useState() + + const userState = useAppSelector((state) => state.user) + + const [invoices, setInvoices] = useState>() + + useDidMount(async () => { + const metadataController = await MetadataController.getInstance() + metadataController.findMetadata(pubkey).then((res) => { + setAuthor(res) + }) + + metadataController.findAdminMetadata().then((res) => { + setAdmin(res) + }) + }) + + const handleAuthorPercentageChange = ( + e: React.ChangeEvent + ) => { + const newValue = parseInt(e.target.value) + setAuthorPercentage(newValue) + setAdminPercentage(100 - newValue) + } + + const handleAdminPercentageChange = ( + e: React.ChangeEvent + ) => { + const newValue = parseInt(e.target.value) + setAdminPercentage(newValue) + setAuthorPercentage(100 - newValue) // Update the other slider to maintain 100% + } + + const handleAmountChange = (event: React.ChangeEvent) => { + const unformattedValue = unformatNumber(event.target.value) + setAmount(unformattedValue) + } + + const generatePaymentInvoices = async () => { + if (!amount) return null + + let userHexKey: string + + setIsLoading(true) + setLoadingSpinnerDesc('Getting user pubkey') + + if (userState.auth && userState.user?.pubkey) { + userHexKey = userState.user.pubkey as string + } else { + userHexKey = (await window.nostr?.getPublicKey()) as string + } + + if (!userHexKey) { + setIsLoading(false) + toast.error('Could not get pubkey') + return null + } + + const adminShare = Math.floor((amount * adminPercentage) / 100) + const authorShare = amount - adminShare + + const zapController = ZapController.getInstance() + + const invoices = new Map() + + if (authorShare > 0 && author?.pubkey && author?.lud16) { + setLoadingSpinnerDesc('Generating invoice for author') + const invoice = await zapController + .getLightningPaymentRequest( + author.lud16, + authorShare, + author.pubkey as string, + userHexKey, + message, + eventId, + aTag + ) + .catch((err) => { + toast.error(err.message || err) + return null + }) + + if (invoice) { + invoices.set('author', invoice) + } + } + + if (adminShare > 0 && admin?.pubkey && admin?.lud16) { + setLoadingSpinnerDesc('Generating invoice for site owner') + const invoice = await zapController + .getLightningPaymentRequest( + admin.lud16, + adminShare, + admin.pubkey as string, + userHexKey, + message, + eventId, + aTag + ) + .catch((err) => { + toast.error(err.message || err) + return null + }) + + if (invoice) { + invoices.set('admin', invoice) + } + } + + setIsLoading(false) + + return invoices + } + + const handleGenerateQRCode = async () => { + const paymentInvoices = await generatePaymentInvoices() + + if (!paymentInvoices) return + + setInvoices(paymentInvoices) + } + + const handleSend = async () => { + const paymentInvoices = await generatePaymentInvoices() + + if (!paymentInvoices) return + + setIsLoading(true) + setLoadingSpinnerDesc('Sending payment!') + + const zapController = ZapController.getInstance() + + if (await zapController.isWeblnProviderExists()) { + const authorInvoice = paymentInvoices.get('author') + if (authorInvoice) { + setLoadingSpinnerDesc('Sending payment to author') + + const sats = parseInt(getTagValue(authorInvoice, 'amount')![0]) / 1000 + + await zapController + .sendPayment(authorInvoice.pr) + .then(() => { + toast.success(`Successfully sent ${sats} sats to author!`) + if (setTotalZapAmount) { + setTotalZapAmount((prev) => prev + sats) + + if (setHasZapped) setHasZapped(true) + } + }) + .catch((err) => { + toast.error(err.message || err) + }) + } + + const adminInvoice = paymentInvoices.get('admin') + if (adminInvoice) { + setLoadingSpinnerDesc('Sending payment to site owner') + + const sats = parseInt(getTagValue(adminInvoice, 'amount')![0]) / 1000 + + await zapController + .sendPayment(adminInvoice.pr) + .then(() => { + toast.success(`Successfully sent ${sats} sats to site owner!`) + }) + .catch((err) => { + toast.error(err.message || err) + }) + } + + handleClose() + } else { + toast.warn('Webln is not present. Use QR code to send zap.') + setInvoices(paymentInvoices) + } + } + + const removeInvoice = (key: string) => { + setInvoices((prev) => { + const newMap = new Map(prev) + newMap.delete(key) + return newMap + }) + } + + const displayQR = () => { + if (!invoices) return null + + const authorInvoice = invoices.get('author') + if (authorInvoice) { + return ( + removeInvoice('author')} + handleQRExpiry={() => removeInvoice('author')} + setTotalZapAmount={setTotalZapAmount} + setHasZapped={setHasZapped} + /> + ) + } + + const adminInvoice = invoices.get('admin') + if (adminInvoice) { + return ( + { + removeInvoice('admin') + handleClose() + }} + handleQRExpiry={() => removeInvoice('admin')} + /> + ) + } + + return null + } + + const authorName = author?.displayName || author?.name || '[name not set up]' + const adminName = admin?.displayName || admin?.name || '[name not set up]' + + return ( + <> + {isLoading && } +
+
+
+
+
+
+

Tip/Zap

+
+
+ + + +
+
+
+
+
+
+ + +
+
+ +
+
+
+ + setMessage(e.target.value)} + /> +
+
+ +
+
+
+
+

+ {authorName} +

+ {author?.nip05 && ( +

+ {author.nip05} +

+ )} +
+
+
+ +

+ {authorPercentage}% +

+
+

+ This goes to show your appreciation to the mod creator! +

+
+
+
+
+
+

+ {adminName} +

+ {admin?.nip05 && ( +

+ )} +
+
+
+ +

+ {adminPercentage}% +

+
+

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

+
+
+ + {displayQR()} +
+
+
+
+
+
+ + ) +} diff --git a/src/pages/mod/internal/zap/index.tsx b/src/pages/mod/internal/zap/index.tsx index ba6d8e9..93e8bad 100644 --- a/src/pages/mod/internal/zap/index.tsx +++ b/src/pages/mod/internal/zap/index.tsx @@ -1,11 +1,10 @@ -import { LoadingSpinner } from 'components/LoadingSpinner' -import { ZapButtons, ZapPopUp, ZapPresets, ZapQR } from 'components/Zap' -import { MetadataController, RelayController, ZapController } from 'controllers' +import { ZapSplit } from 'components/Zap' +import { RelayController } from 'controllers' import { useAppSelector, useDidMount } from 'hooks' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { toast } from 'react-toastify' -import { ModDetails, PaymentRequest } from 'types' -import { abbreviateNumber, formatNumber, unformatNumber } from 'utils' +import { ModDetails } from 'types' +import { abbreviateNumber } from 'utils' type ZapProps = { modDetails: ModDetails @@ -65,192 +64,15 @@ export const Zap = ({ modDetails }: ZapProps) => { {isOpen && ( - setIsOpen(false)} - lastNode={} - notCloseAfterZap setTotalZapAmount={setTotalZappedAmount} setHasZapped={setHasZapped} + handleClose={() => setIsOpen(false)} /> )} ) } - -const ZapSite = () => { - const [isLoading, setIsLoading] = useState(false) - const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - - const [amount, setAmount] = useState(0) - - const [paymentRequest, setPaymentRequest] = useState() - - const userState = useAppSelector((state) => state.user) - - const handleAmountChange = (event: React.ChangeEvent) => { - 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 => { - 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 - ) - .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 ( - <> -
- -
-
-
-
-

DEG Mods

-

- degmods@degmods.com -

-
-
-

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

-
- - -
-
- -
- - {paymentRequest && ( - - )} -
-
- {isLoading && } - - ) -}