feat: on-chain handoff from the custom-zap dialog

Adds a "Send on-chain instead" link button at the bottom of
ZapCustomDialog. Tapping it opens OnchainZapSendDialog with the entered
amount and message prefilled, the note as zappedEvent, and the same
split-detection / dust-preview / fee-tier picker that the Reactions
zap chip uses.

This also fixes the participant zap path in nests audio rooms
(ParticipantHostActionsSheet long-press → "Zap") since that menu item
opens ZapCustomDialog under the hood — fixing the custom dialog covers
both entry points transitively.

OnchainZapSendDialog gains a `prefillComment: String = ""` parameter so
the message field carries over from the LN dialog.

Poll-note zaps (FilteredZapAmountChoicePopup) intentionally stay
Lightning-only — the poll_option vote is encoded in the kind:9734 zap
request and counted via kind:9735 receipts; kind:8333 on-chain
receipts have no poll_option analog and the count infrastructure
doesn't index them. This is a protocol design constraint, not a UI
oversight.

Coverage audit summary — every user-facing zap initiation point now
has on-chain support except the poll-voting path:
- ZapReaction (reactions row) 
- ZapAmountChoicePopup (popup chips) 
- ZapCustomDialog (custom amount + message)  (this commit)
- ReusableZapButton (Zap the Devs + DVM buttons) 
- NestActionBar (audio room) 
- ParticipantHostActionsSheet (audio room participant)  (transitive)
- ChatMessageCompose / live-activity headers (use ZapReaction) 
- FilteredZapAmountChoicePopup (poll votes) ⚠️ LN-only by protocol
This commit is contained in:
Claude
2026-05-20 20:35:01 +00:00
parent a90dd47ed4
commit 85dfe93ea7
3 changed files with 40 additions and 1 deletions
@@ -48,6 +48,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -81,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
@@ -89,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
@@ -157,6 +160,11 @@ fun ZapCustomDialog(
val presetAmounts = remember(accountViewModel) { accountViewModel.zapAmountChoices() }
// True while the on-chain hand-off sheet is open. Dismissing the on-chain
// sheet closes the whole zap flow — the user has already committed to a
// payment method, no "go back to Lightning" is offered here.
var sendOnchain by remember { mutableStateOf(false) }
Dialog(
onDismissRequest = { onClose() },
properties =
@@ -326,9 +334,38 @@ fun ZapCustomDialog(
)
onClose()
}
// Hand off to the on-chain dialog with the entered amount +
// message prefilled. Disabled while the amount is empty so the
// user can't open a sheet that immediately gates on its own
// empty field.
TextButton(
onClick = { sendOnchain = true },
enabled = postViewModel.canSend() && !baseNote.isDraft(),
modifier =
Modifier
.fillMaxWidth()
.padding(top = 4.dp),
) {
Text(text = stringRes(id = R.string.send_onchain_instead))
}
}
}
}
if (sendOnchain) {
OnchainZapSendDialog(
accountViewModel = accountViewModel,
onDismiss = {
sendOnchain = false
onClose()
},
recipientPubKey = baseNote.author?.pubkeyHex,
zappedEvent = baseNote.toEventHint<Event>(),
prefillAmountSats = postViewModel.value(),
prefillComment = postViewModel.customMessage.text,
)
}
}
@Composable
@@ -137,6 +137,7 @@ fun OnchainZapSendDialog(
recipientPubKey: HexKey? = null,
zappedEvent: EventHintBundle<out Event>? = null,
prefillAmountSats: Long? = null,
prefillComment: String = "",
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
@@ -152,7 +153,7 @@ fun OnchainZapSendDialog(
var searchInput by remember { mutableStateOf("") }
var selectedUser by remember { mutableStateOf<User?>(null) }
var amountInput by remember { mutableStateOf(prefillAmountSats?.toString().orEmpty()) }
var comment by remember { mutableStateOf("") }
var comment by remember { mutableStateOf(prefillComment) }
var feeTier by remember { mutableStateOf(FeeTier.NORMAL) }
var fees by remember { mutableStateOf<FeeEstimates?>(null) }
+1
View File
@@ -824,6 +824,7 @@
<string name="quick_zap_amounts_onchain">Quick On-chain Zap Amounts</string>
<string name="quick_zap_amounts_onchain_explainer">On-chain zaps pay miner fees, so amounts are usually larger than Lightning. Tap an amount to remove it.</string>
<string name="new_amount_in_sats_onchain">New on-chain amount in sats</string>
<string name="send_onchain_instead">Send on-chain instead</string>
<string name="zap_privacy_section">Zap Privacy</string>
<string name="zap_type_section_explainer">Controls how your identity is shown when you send a zap.</string>
<string name="wallet_connect_connect_app">Connect Wallet</string>