diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx
index 33859bd8..cc023555 100644
--- a/src/components/Content/index.tsx
+++ b/src/components/Content/index.tsx
@@ -3,6 +3,7 @@ import {
EmbeddedEventParser,
EmbeddedHashtagParser,
EmbeddedImageParser,
+ EmbeddedLNInvoiceParser,
EmbeddedMentionParser,
EmbeddedNormalUrlParser,
EmbeddedVideoParser,
@@ -19,6 +20,7 @@ import { memo } from 'react'
import {
EmbeddedHashtag,
EmbeddedMention,
+ EmbeddedLNInvoice,
EmbeddedNormalUrl,
EmbeddedNote,
EmbeddedWebsocketUrl
@@ -33,6 +35,7 @@ const Content = memo(({ event, className }: { event: Event; className?: string }
EmbeddedImageParser,
EmbeddedVideoParser,
EmbeddedNormalUrlParser,
+ EmbeddedLNInvoiceParser,
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
@@ -101,6 +104,9 @@ const Content = memo(({ event, className }: { event: Event; className?: string }
if (node.type === 'url') {
return
}
+ if (node.type === 'invoice') {
+ return
+ }
if (node.type === 'websocket-url') {
return
}
diff --git a/src/components/Embedded/EmbeddedLNInvoice.tsx b/src/components/Embedded/EmbeddedLNInvoice.tsx
new file mode 100644
index 00000000..e68297a9
--- /dev/null
+++ b/src/components/Embedded/EmbeddedLNInvoice.tsx
@@ -0,0 +1,84 @@
+import { formatAmount, getAmountFromInvoice } from '@/lib/lightning'
+import { cn } from '@/lib/utils'
+import { useNostr } from '@/providers/NostrProvider'
+import { useToast } from '@/hooks'
+import { Loader, Zap } from 'lucide-react'
+import lightning from '@/services/lightning.service'
+import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button } from '@/components/ui/button'
+
+export function EmbeddedLNInvoice({ invoice }: { invoice: string }) {
+ const { t } = useTranslation()
+ const { toast } = useToast()
+ const { checkLogin, pubkey } = useNostr()
+ const [paying, setPaying] = useState(false)
+
+ const amount = useMemo(() => {
+ return getAmountFromInvoice(invoice)
+ }, [invoice])
+
+ const handlePay = async () => {
+ try {
+ if (!pubkey) {
+ throw new Error('You need to be logged in to zap')
+ }
+ setPaying(true)
+ const invoiceResult = await lightning.payInvoice(invoice)
+ // user canceled
+ if (!invoiceResult) {
+ return
+ }
+ } catch (error) {
+ toast({
+ title: t('Lightning payment failed'),
+ description: (error as Error).message,
+ variant: 'destructive'
+ })
+ } finally {
+ setPaying(false)
+ }
+ }
+
+ const handlePayClick = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ checkLogin(() => handlePay())
+ }
+
+ return (
+
e.stopPropagation()}
+ >
+
+
+
Lightning Invoice
+
+
+ {formatAmount(amount)}
+
+
+
+ )
+}
diff --git a/src/components/Embedded/index.tsx b/src/components/Embedded/index.tsx
index 7c634ddf..f3f8a43d 100644
--- a/src/components/Embedded/index.tsx
+++ b/src/components/Embedded/index.tsx
@@ -1,4 +1,5 @@
export * from './EmbeddedHashtag'
+export * from './EmbeddedLNInvoice'
export * from './EmbeddedMention'
export * from './EmbeddedNormalUrl'
export * from './EmbeddedNote'
diff --git a/src/components/PictureContent/index.tsx b/src/components/PictureContent/index.tsx
index 82ccfa20..276ed3b8 100644
--- a/src/components/PictureContent/index.tsx
+++ b/src/components/PictureContent/index.tsx
@@ -1,5 +1,6 @@
import {
EmbeddedEmojiParser,
+ EmbeddedLNInvoiceParser,
EmbeddedHashtagParser,
EmbeddedMentionParser,
EmbeddedNormalUrlParser,
@@ -12,6 +13,7 @@ import { Event } from 'nostr-tools'
import { memo, useMemo } from 'react'
import {
EmbeddedHashtag,
+ EmbeddedLNInvoice,
EmbeddedMention,
EmbeddedNormalUrl,
EmbeddedWebsocketUrl
@@ -25,6 +27,7 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s
const nodes = parseContent(event.content, [
EmbeddedNormalUrlParser,
+ EmbeddedLNInvoiceParser,
EmbeddedWebsocketUrlParser,
EmbeddedHashtagParser,
EmbeddedMentionParser,
@@ -44,6 +47,9 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s
if (node.type === 'url') {
return
}
+ if (node.type === 'invoice') {
+ return
+ }
if (node.type === 'websocket-url') {
return
}
diff --git a/src/constants.ts b/src/constants.ts
index 4f292a82..8857dcf8 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -61,6 +61,7 @@ export const EMOJI_SHORT_CODE_REGEX = /:[a-zA-Z0-9_-]+:/g
export const EMBEDDED_EVENT_REGEX = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g
export const EMBEDDED_MENTION_REGEX = /nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+)/g
export const HASHTAG_REGEX = /#[\p{L}\p{N}\p{M}_]+/gu
+export const LN_INVOICE_REGEX = /(ln(?:bc|tb|bcrt))([0-9]+[munp]?)?1([02-9ac-hj-np-z]+)/g
export const MONITOR = '9bbbb845e5b6c831c29789900769843ab43bb5047abe697870cb50b6fc9bf923'
export const MONITOR_RELAYS = ['wss://relay.nostr.watch/']
diff --git a/src/lib/content-parser.ts b/src/lib/content-parser.ts
index 8e2e9095..24a4977b 100644
--- a/src/lib/content-parser.ts
+++ b/src/lib/content-parser.ts
@@ -4,6 +4,7 @@ import {
EMOJI_SHORT_CODE_REGEX,
HASHTAG_REGEX,
IMAGE_REGEX,
+ LN_INVOICE_REGEX,
URL_REGEX,
VIDEO_REGEX,
WS_URL_REGEX
@@ -21,6 +22,7 @@ export type TEmbeddedNodeType =
| 'websocket-url'
| 'url'
| 'emoji'
+ | 'invoice'
export type TEmbeddedNode =
| {
@@ -79,6 +81,11 @@ export const EmbeddedEmojiParser: TContentParser = {
regex: EMOJI_SHORT_CODE_REGEX
}
+export const EmbeddedLNInvoiceParser: TContentParser = {
+ type: 'invoice',
+ regex: LN_INVOICE_REGEX
+}
+
export function parseContent(content: string, parsers: TContentParser[]) {
let nodes: TEmbeddedNode[] = [{ type: 'text', data: content.trim() }]
diff --git a/src/services/lightning.service.ts b/src/services/lightning.service.ts
index 3842e4b1..4b6265f2 100644
--- a/src/services/lightning.service.ts
+++ b/src/services/lightning.service.ts
@@ -154,6 +154,27 @@ class LightningService {
})
}
+ async payInvoice(invoice: string, closeOuterModel?: () => void): Promise<{ preimage: string; invoice: string } | null> {
+ if (this.provider) {
+ const { preimage } = await this.provider.sendPayment(invoice)
+ closeOuterModel?.()
+ return { preimage, invoice: invoice }
+ }
+
+ return new Promise((resolve) => {
+ closeOuterModel?.()
+ launchPaymentModal({
+ invoice: invoice,
+ onPaid: (response) => {
+ resolve({ preimage: response.preimage, invoice: invoice })
+ },
+ onCancelled: () => {
+ resolve(null)
+ }
+ })
+ })
+ }
+
async fetchRecentSupporters() {
if (this.recentSupportersCache) {
return this.recentSupportersCache