feat: add comment support for Lightning payments in BTCPay and Strike services

- Enhanced the sendPaymentWithAddress method in BTCPayService and StrikeService to accept an optional comment parameter.
- Updated resolveLightningAddress to include the comment in the callback URL if supported by the LNURL-pay endpoint.
- Modified PaymentService to construct a descriptive comment for Lightning invoices, improving clarity for users.

These changes enhance the payment experience by allowing users to include contextual information with their transactions.
This commit is contained in:
Dorian
2026-02-14 13:02:42 +00:00
parent e774d20821
commit 11d289d793
8 changed files with 135 additions and 35 deletions

View File

@@ -269,7 +269,28 @@
{{ userInitials }}
</div>
<span class="text-white text-sm font-medium">{{ userName }}</span>
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': dropdownOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div v-if="dropdownOpen" class="profile-menu absolute right-0 mt-2 w-48">
<div class="floating-glass-header py-2 rounded-xl">
<button @click="navigateTo('/profile')" class="profile-menu-item flex items-center gap-3 px-4 py-2.5 w-full text-left">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span>Profile</span>
</button>
<div class="border-t border-white/10 my-1"></div>
<button @click="handleLogout" class="profile-menu-item flex items-center gap-3 px-4 py-2.5 text-red-400 w-full text-left">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span>Sign Out</span>
</button>
</div>
</div>
</div>
<!-- Mobile -->
@@ -374,6 +395,29 @@ const hasNostrSession = computed(() => {
return !!(sessionStorage.getItem('nostr_token') || sessionStorage.getItem('auth_token'))
})
/**
* Auto-detect stale auth state: backend thinks we're authenticated
* (sessionStorage tokens exist) but accountManager has no active
* Nostr account (localStorage was cleared). This causes a dead-end
* "Nostr" profile button with no dropdown. Fix by auto-logging out.
*/
watch(
[() => isAuthenticated.value, () => nostrLoggedIn.value],
([authed, nostr]) => {
if (authed && !nostr) {
// Only auto-logout when the auth type is nostr-based —
// check if there's a nostr token but no active Nostr account
const hasNostrToken = !!sessionStorage.getItem('nostr_token')
const hasNostrPubkey = !!sessionStorage.getItem('nostr_pubkey')
if (hasNostrToken || hasNostrPubkey) {
console.warn('[AppHeader] Stale Nostr session detected (tokens exist but no active account). Auto-logging out.')
handleLogout()
}
}
},
{ immediate: true },
)
/** Switch content source and reload */
function handleSourceSelect(sourceId: string) {
contentSourceStore.setSource(sourceId as any)

View File

@@ -64,27 +64,29 @@
</div>
</div>
<!-- Nostr Login Button (NIP-07 Browser Extension) -->
<button
@click="handleNostrLogin"
:disabled="isLoading"
class="nostr-login-button w-full flex items-center justify-center gap-2"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
Sign in with Nostr Extension
</button>
<!-- Nostr Login Button (NIP-07 Browser Extension) only shown when extension is installed -->
<template v-if="hasNostrExtension">
<button
@click="handleNostrLogin"
:disabled="isLoading"
class="nostr-login-button w-full flex items-center justify-center gap-2"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
Sign in with Nostr Extension
</button>
<!-- Divider between Nostr methods -->
<div class="relative my-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-white/10"></div>
<!-- Divider between Nostr methods -->
<div class="relative my-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-white/10"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-4 bg-transparent text-white/30 text-xs">or</span>
</div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-4 bg-transparent text-white/30 text-xs">or</span>
</div>
</div>
</template>
<!-- Amber Login (NIP-55 Android Signer) 3-phase flow -->
<button
@@ -254,6 +256,9 @@ const mode = ref<'login' | 'register' | 'forgot'>(props.defaultMode)
const errorMessage = ref<string | null>(null)
const isLoading = computed(() => authLoading.value || sovereignGenerating.value)
// NIP-07 extension detection — hide the button entirely when no extension is installed
const hasNostrExtension = ref(!!(window as any).nostr)
// Amber three-phase login state
const amberPhase = ref<'idle' | 'waiting-pubkey' | 'waiting-signature'>('idle')
const amberPubkey = ref<string | null>(null)

View File

@@ -10,8 +10,22 @@
</svg>
</button>
<!-- NO CREATOR STATE -->
<template v-if="noCreator">
<div class="text-center py-4">
<div class="text-5xl mb-5">🤷</div>
<h2 class="text-2xl font-bold text-white mb-3">This is free content</h2>
<p class="text-white/50 text-sm mb-6 max-w-xs mx-auto leading-relaxed">
No creator to zap this content doesn't have a Lightning-enabled creator on IndeeHub yet.
</p>
<button @click="closeModal" class="zap-pay-button w-full flex items-center justify-center">
Got it
</button>
</div>
</template>
<!-- ─── AMOUNT SELECTION STATE ─── -->
<template v-if="paymentState === 'select'">
<template v-else-if="paymentState === 'select'">
<div class="text-center mb-6">
<div class="w-14 h-14 mx-auto mb-4 rounded-full bg-[#F7931A]/15 flex items-center justify-center">
<svg class="w-7 h-7 text-[#F7931A]" viewBox="0 0 24 24" fill="currentColor">
@@ -83,7 +97,7 @@
</template>
<!-- ─── INVOICE STATE: QR Code + BOLT11 ─── -->
<template v-if="paymentState === 'invoice'">
<template v-else-if="paymentState === 'invoice'">
<div class="text-center">
<h2 class="text-2xl font-bold text-white mb-2">Pay with Lightning</h2>
<p class="text-white/50 text-sm mb-6">Scan the QR code or copy the invoice</p>
@@ -132,7 +146,7 @@
</template>
<!-- ─── SUCCESS STATE: Lightning Celebration ─── -->
<template v-if="paymentState === 'success'">
<template v-else-if="paymentState === 'success'">
<div class="text-center py-4 relative overflow-hidden">
<!-- Animated lightning bolts background -->
<div class="zap-celebration">
@@ -213,6 +227,8 @@ const customActive = ref(false)
const isLoading = ref(false)
const errorMessage = ref<string | null>(null)
const creatorName = ref<string | null>(null)
/** True when the content has no zappable creator (free/partner content) */
const noCreator = ref(false)
const successQuote = ref('')
// LNURL-pay data
@@ -286,6 +302,7 @@ watch(() => props.isOpen, async (open) => {
qrCodeDataUrl.value = ''
copyButtonText.value = 'Copy Invoice'
creatorName.value = props.content?.creator || null
noCreator.value = false
lnurlCallback.value = null
verifyUrl.value = null
successQuote.value = ''
@@ -311,12 +328,21 @@ async function resolveLightningAddress() {
try {
const projectId = props.content.id
const ownerData = await indeehubApiService.get<{
id: string
professionalName?: string
}>(`/filmmakers/project/${projectId}/owner`)
let ownerData: { id: string; professionalName?: string } | null = null
if (!ownerData?.id) return
try {
ownerData = await indeehubApiService.get<{
id: string
professionalName?: string
}>(`/filmmakers/project/${projectId}/owner`)
} catch {
// API returned error (e.g. 404 for free/partner content)
}
if (!ownerData?.id) {
noCreator.value = true
return
}
if (ownerData.professionalName) {
creatorName.value = ownerData.professionalName
@@ -327,7 +353,10 @@ async function resolveLightningAddress() {
}>(`/filmmakers/${ownerData.id}/lightning-address`)
const lightningAddress = addressData?.lightningAddress
if (!lightningAddress) return
if (!lightningAddress) {
noCreator.value = true
return
}
const [username, domain] = lightningAddress.split('@')
if (!username || !domain) return
@@ -345,6 +374,7 @@ async function resolveLightningAddress() {
lnurlMaxSats.value = Math.floor((lnurlData.maxSendable || 10_000_000_000) / 1000)
} catch (err) {
console.warn('[ZapModal] Failed to resolve lightning address:', err)
noCreator.value = true
}
}