Browse Source

feat: render embedded invoices (#392)

imwald
Daniel Vergara 9 months ago committed by GitHub
parent
commit
f25b742877
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      src/components/Content/index.tsx
  2. 84
      src/components/Embedded/EmbeddedLNInvoice.tsx
  3. 1
      src/components/Embedded/index.tsx
  4. 6
      src/components/PictureContent/index.tsx
  5. 1
      src/constants.ts
  6. 7
      src/lib/content-parser.ts
  7. 21
      src/services/lightning.service.ts

6
src/components/Content/index.tsx

@ -3,6 +3,7 @@ import { @@ -3,6 +3,7 @@ import {
EmbeddedEventParser,
EmbeddedHashtagParser,
EmbeddedImageParser,
EmbeddedLNInvoiceParser,
EmbeddedMentionParser,
EmbeddedNormalUrlParser,
EmbeddedVideoParser,
@ -19,6 +20,7 @@ import { memo } from 'react' @@ -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 } @@ -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 } @@ -101,6 +104,9 @@ const Content = memo(({ event, className }: { event: Event; className?: string }
if (node.type === 'url') {
return <EmbeddedNormalUrl url={node.data} key={index} />
}
if (node.type === 'invoice') {
return <EmbeddedLNInvoice invoice={node.data} key={index} />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl url={node.data} key={index} />
}

84
src/components/Embedded/EmbeddedLNInvoice.tsx

@ -0,0 +1,84 @@ @@ -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 (
<div
className={cn(
'border rounded-lg p-4 bg-card text-card-foreground shadow-sm',
'flex flex-col gap-3 my-2 max-w-sm'
)}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-2">
<Zap className="w-5 h-5 text-yellow-500" />
<h3 className="font-semibold text-sm">Lightning Invoice</h3>
</div>
<div className="text-lg font-bold">
{formatAmount(amount)}
</div>
<Button
className={cn(
'w-full px-4 py-2 rounded-md font-medium text-sm',
'bg-purple-600 hover:bg-purple-700 text-white',
'disabled:opacity-50 disabled:cursor-not-allowed',
'transition-colors duration-200',
'flex items-center justify-center gap-2'
)}
onClick={handlePayClick}
>
{paying ? (
<>
<Loader className="w-4 h-4 animate-spin" />
Paying...
</>
) : (
'Pay'
)}
</Button>
</div>
)
}

1
src/components/Embedded/index.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
export * from './EmbeddedHashtag'
export * from './EmbeddedLNInvoice'
export * from './EmbeddedMention'
export * from './EmbeddedNormalUrl'
export * from './EmbeddedNote'

6
src/components/PictureContent/index.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import {
EmbeddedEmojiParser,
EmbeddedLNInvoiceParser,
EmbeddedHashtagParser,
EmbeddedMentionParser,
EmbeddedNormalUrlParser,
@ -12,6 +13,7 @@ import { Event } from 'nostr-tools' @@ -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 @@ -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 @@ -44,6 +47,9 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s
if (node.type === 'url') {
return <EmbeddedNormalUrl key={index} url={node.data} />
}
if (node.type === 'invoice') {
return <EmbeddedLNInvoice invoice={node.data} key={index} />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl key={index} url={node.data} />
}

1
src/constants.ts

@ -61,6 +61,7 @@ export const EMOJI_SHORT_CODE_REGEX = /:[a-zA-Z0-9_-]+:/g @@ -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/']

7
src/lib/content-parser.ts

@ -4,6 +4,7 @@ import { @@ -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 = @@ -21,6 +22,7 @@ export type TEmbeddedNodeType =
| 'websocket-url'
| 'url'
| 'emoji'
| 'invoice'
export type TEmbeddedNode =
| {
@ -79,6 +81,11 @@ export const EmbeddedEmojiParser: TContentParser = { @@ -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() }]

21
src/services/lightning.service.ts

@ -154,6 +154,27 @@ class LightningService { @@ -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

Loading…
Cancel
Save