diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt index 569ec04c1d..d511ee9d29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt @@ -52,13 +52,14 @@ object ClinkOfferPayer { account: Account, offer: NOffer, amountSats: Long? = null, + zap: String? = null, timeoutMs: Long = DEFAULT_TIMEOUT_MS, ): OfferResponse? { val relays = offer.relays.toSet() if (relays.isEmpty()) return null val client = OfferClient(offer, account.signer) - val request = client.requestInvoice(amountSats) + val request = client.requestInvoice(amountSats = amountSats, zap = zap) val reply = CompletableDeferred() val subId = "clink-offer-${request.id}" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 0e617b8750..9fd0a2aa74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -368,6 +368,7 @@ private fun RenderRegular( callbackUri, accountViewModel, nav, + authorPubKey, ) } } @@ -381,6 +382,7 @@ private fun RenderRegular( backgroundColor, accountViewModel, nav, + authorPubKey, ) } } @@ -476,6 +478,7 @@ private fun RenderWordWithoutPreview( backgroundColor: MutableState, accountViewModel: AccountViewModel, nav: INav, + authorPubKey: String? = null, ) { when (word) { // Don't preview Images @@ -510,7 +513,7 @@ private fun RenderWordWithoutPreview( // Decoding is local and the network round-trip only fires on the Pay tap, // so the offer card is safe to render even in the no-preview path. - is ClinkOfferSegment -> ClinkOfferPreview(word.offer, accountViewModel) + is ClinkOfferSegment -> ClinkOfferPreview(word.offer, accountViewModel, authorPubKey) is EmailSegment -> ClickableEmail(word.segmentText) @@ -547,6 +550,7 @@ private fun RenderWordWithPreview( callbackUri: String? = null, accountViewModel: AccountViewModel, nav: INav, + authorPubKey: String? = null, ) { when (word) { is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel) @@ -558,7 +562,7 @@ private fun RenderWordWithPreview( is InvoiceSegment -> MayBeInvoicePreview(word.segmentText, accountViewModel) is WithdrawSegment -> MayBeWithdrawal(word.segmentText, accountViewModel) is CashuSegment -> CashuPreview(word.segmentText, accountViewModel) - is ClinkOfferSegment -> ClinkOfferPreview(word.offer, accountViewModel) + is ClinkOfferSegment -> ClinkOfferPreview(word.offer, accountViewModel, authorPubKey) is EmailSegment -> ClickableEmail(word.segmentText) is SecretEmoji -> DisplaySecretEmoji(word, state, callbackUri, true, quotesLeft, backgroundColor, accountViewModel, nav) is MathSegment -> LatexEquation(word.latex, word.displayMode, word.leading, word.trailing) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt index 43c7584237..fac3d2a7eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.experimental.clink.common.SatRange import com.vitorpamplona.quartz.experimental.clink.offers.OfferErrorCode import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer import com.vitorpamplona.quartz.experimental.clink.pointers.OfferPriceType +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.coroutines.launch /** @@ -70,11 +71,18 @@ import kotlinx.coroutines.launch * runs the offer round-trip ([ClinkOfferPayer]) to fetch a fresh BOLT-11 over Nostr, * then pays it through the user's default payment source (confirmed for in-app wallets, * see [InvoicePaymentDispatcher]). + * + * When [authorPubKey] is known (the offer appears in someone's note) and the user's + * default zap type isn't NONZAP, the request carries a NIP-57 zap request so the offer + * service issues a zappable invoice and publishes a zap receipt — making the payment a + * real zap on the author rather than a silent invoice. With no author it falls back to + * a plain invoice. */ @Composable fun ClinkOfferPreview( offer: NOffer, accountViewModel: AccountViewModel, + authorPubKey: String? = null, ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -191,8 +199,26 @@ fun ClinkOfferPreview( onClick = { requesting = true scope.launch { - val amount = if (amountRequired) amountInput.toLongOrNull() else null - val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, offer, amountSats = amount) + val amount = if (amountRequired) amountInput.toLongOrNull() else offer.price?.toLong() + + // Attach a NIP-57 zap request so paying the offer becomes a real zap + // on the author (skipped when there's no author or the user opted out + // of zaps via a NONZAP default). + val zapType = accountViewModel.defaultZapType() + val zapRequest = + if (authorPubKey != null && zapType != LnZapEvent.ZapType.NONZAP) { + val author = accountViewModel.account.cache.getOrCreateUser(authorPubKey) + accountViewModel.account + .createZapRequestFor( + user = author, + zapType = zapType, + amountMillisats = amount?.times(1000), + ).toJson() + } else { + null + } + + val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, offer, amountSats = amount, zap = zapRequest) requesting = false val bolt11 = response?.bolt11