feat: add Silent Payment (BIP 352) support to kind 0 profiles (#758)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: BoltTouring <BoltTouring@users.noreply.github.com> Co-authored-by: Tanjiro <claude@tricycle.cc>
This commit is contained in:
parent
4c144c3da1
commit
071c740145
5 changed files with 131 additions and 2 deletions
|
|
@ -14,11 +14,12 @@ import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
|
|||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Link, Zap } from 'lucide-react'
|
||||
import { Link, Zap, Bitcoin, Check, Copy } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NotFound from '../NotFound'
|
||||
import SearchInput from '../SearchInput'
|
||||
import SpQrCode from '../SpQrCode'
|
||||
import TextWithEmojis from '../TextWithEmojis'
|
||||
import TrustScoreBadge from '../TrustScoreBadge'
|
||||
import AvatarWithLightbox from './AvatarWithLightbox'
|
||||
|
|
@ -112,7 +113,7 @@ export default function Profile({ id }: { id?: string }) {
|
|||
}
|
||||
if (!profile) return <NotFound />
|
||||
|
||||
const { banner, username, about, pubkey, website, lightningAddress, emojis } = profile
|
||||
const { banner, username, about, pubkey, website, lightningAddress, sp, emojis } = profile
|
||||
return (
|
||||
<>
|
||||
<div ref={topContainerRef}>
|
||||
|
|
@ -160,6 +161,13 @@ export default function Profile({ id }: { id?: string }) {
|
|||
<div className="w-0 max-w-fit flex-1 truncate">{lightningAddress}</div>
|
||||
</div>
|
||||
)}
|
||||
{sp && (
|
||||
<div className="flex select-text items-center gap-1 text-sm text-orange-500">
|
||||
<Bitcoin className="size-4 shrink-0" />
|
||||
<SpCopy sp={sp} />
|
||||
<SpQrCode sp={sp} />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 flex gap-1">
|
||||
<PubkeyCopy pubkey={pubkey} />
|
||||
<NpubQrCode pubkey={pubkey} />
|
||||
|
|
@ -210,3 +218,24 @@ export default function Profile({ id }: { id?: string }) {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SpCopy({ sp }: { sp: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const truncated = sp.length > 24 ? sp.slice(0, 12) + '...' + sp.slice(-6) : sp
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(sp)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="clickable flex w-fit items-center gap-1 font-mono text-xs"
|
||||
onClick={copy}
|
||||
>
|
||||
<div>{truncated}</div>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
64
src/components/SpQrCode/index.tsx
Normal file
64
src/components/SpQrCode/index.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { Bitcoin, Check, Copy, QrCodeIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import QrCode from '../QrCode'
|
||||
|
||||
export default function SpQrCode({ sp }: { sp: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
if (!sp) return null
|
||||
|
||||
const truncated = sp.length > 24 ? sp.slice(0, 12) + '...' + sp.slice(-6) : sp
|
||||
|
||||
const copySp = () => {
|
||||
navigator.clipboard.writeText(sp)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const trigger = (
|
||||
<div className="flex h-5 w-5 flex-col items-center justify-center rounded-full bg-muted text-muted-foreground hover:text-foreground">
|
||||
<QrCodeIcon size={14} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const content = (
|
||||
<div className="flex w-full flex-col items-center gap-4 p-8">
|
||||
<div className="flex items-center gap-2 text-orange-500">
|
||||
<Bitcoin size={24} />
|
||||
<div className="text-lg font-semibold">{t('Silent Payment')}</div>
|
||||
</div>
|
||||
<QrCode size={512} value={sp} />
|
||||
<div
|
||||
className="clickable flex w-fit items-center gap-2 rounded-full bg-muted px-2 text-sm text-muted-foreground"
|
||||
onClick={copySp}
|
||||
>
|
||||
<div className="font-mono text-xs">{truncated}</div>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger>{trigger}</DrawerTrigger>
|
||||
<DrawerContent>{content}</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>{trigger}</DialogTrigger>
|
||||
<DialogContent className="m-0 w-80 p-0" onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
{content}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ export function getProfileFromEvent(event: Event) {
|
|||
lud06: profileObj.lud06,
|
||||
lud16: profileObj.lud16,
|
||||
lightningAddress: getLightningAddressFromProfile(profileObj),
|
||||
sp: profileObj.sp,
|
||||
created_at: event.created_at,
|
||||
emojis: emojis.length > 0 ? emojis : undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||
const [nip05Error, setNip05Error] = useState<string>('')
|
||||
const [lightningAddress, setLightningAddress] = useState<string>('')
|
||||
const [lightningAddressError, setLightningAddressError] = useState<string>('')
|
||||
const [silentPaymentAddress, setSilentPaymentAddress] = useState<string>('')
|
||||
const [silentPaymentAddressError, setSilentPaymentAddressError] = useState<string>('')
|
||||
const [hasChanged, setHasChanged] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [uploadingBanner, setUploadingBanner] = useState(false)
|
||||
|
|
@ -48,6 +50,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||
setWebsite(profile.website ?? '')
|
||||
setNip05(profile.nip05 ?? '')
|
||||
setLightningAddress(profile.lightningAddress || '')
|
||||
setSilentPaymentAddress(profile.sp || '')
|
||||
} else {
|
||||
setBanner('')
|
||||
setAvatar('')
|
||||
|
|
@ -56,6 +59,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||
setWebsite('')
|
||||
setNip05('')
|
||||
setLightningAddress('')
|
||||
setSilentPaymentAddress('')
|
||||
}
|
||||
}, [profile])
|
||||
|
||||
|
|
@ -93,6 +97,17 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||
delete newProfileContent.lud16
|
||||
}
|
||||
|
||||
if (silentPaymentAddress) {
|
||||
if (silentPaymentAddress.startsWith('sp1')) {
|
||||
newProfileContent.sp = silentPaymentAddress
|
||||
} else {
|
||||
setSilentPaymentAddressError(t('Silent Payment address must start with sp1'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
delete newProfileContent.sp
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
setHasChanged(false)
|
||||
const profileDraftEvent = createProfileDraftEvent(
|
||||
|
|
@ -228,6 +243,25 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||
<div className="pl-3 text-xs text-destructive">{lightningAddressError}</div>
|
||||
)}
|
||||
</Item>
|
||||
<Item>
|
||||
<Label htmlFor="profile-sp-address-input">
|
||||
{t('Silent Payment Address (BIP 352)')}
|
||||
</Label>
|
||||
<Input
|
||||
id="profile-sp-address-input"
|
||||
value={silentPaymentAddress}
|
||||
placeholder="sp1qq..."
|
||||
onChange={(e) => {
|
||||
setSilentPaymentAddressError('')
|
||||
setSilentPaymentAddress(e.target.value)
|
||||
setHasChanged(true)
|
||||
}}
|
||||
className={silentPaymentAddressError ? 'border-destructive' : ''}
|
||||
/>
|
||||
{silentPaymentAddressError && (
|
||||
<div className="pl-3 text-xs text-destructive">{silentPaymentAddressError}</div>
|
||||
)}
|
||||
</Item>
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
|
|
|
|||
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
|
|
@ -27,6 +27,7 @@ export type TProfile = {
|
|||
lud06?: string
|
||||
lud16?: string
|
||||
lightningAddress?: string
|
||||
sp?: string
|
||||
created_at?: number
|
||||
emojis?: TEmoji[]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue