mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
feat(clink): make offer payments zappable
When a noffer is rendered in someone's note, paying it now attaches a NIP-57 zap request so the offer service issues a zappable invoice and publishes a zap receipt — turning the payment into a real zap on the author instead of a silent invoice: - ClinkOfferPayer.requestInvoice forwards a serialized zap request via OfferClient's existing zap field. - ClinkOfferPreview builds the 9734 from the threaded authorPubKey using the account's default zap type (skipped for NONZAP), at the resolved amount. - RichTextViewer threads authorPubKey into the offer-segment renderers; the markdown/secret paths default to null (plain invoice, no target). Falls back to a plain invoice when there's no author or the user opted out of zaps. :amethyst compiles; the round-trip and receipt remain untested end-to-end.
This commit is contained in:
@@ -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<OfferEvent>()
|
||||
val subId = "clink-offer-${request.id}"
|
||||
|
||||
@@ -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<Color>,
|
||||
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)
|
||||
|
||||
+28
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user