Files
amethyst/commons
Claude 6cb485cda8 feat(zap): offer a NIP-A3 pay-to hand-off in the zap picker
When the sender and the note's author both publish a payment target of the
same protocol, the zap picker now offers a chip that hands off to the app that
owns it. Gated on a new opt-in setting (default off), on the note carrying no
NIP-57 zap split, and on an installed app actually resolving the URI.

The chip carries no amount. Zap presets are sats and there is no rate anywhere
in the repo to turn them into a Venmo or IBAN figure, so no number is shown and
no RFC-8905 amount= is emitted; the receiving app asks. It ends in OpenInNew
rather than the send arrow every amount segment uses, and long-press copies the
authority instead of opening the sat-preset editor, which would mean nothing
here. Nothing is published, so the zap counter does not move and none of the
zap progress state is touched.

It renders beside the amount pills rather than inside each pill's rail toggle.
Carrying no amount, it would otherwise repeat identically once per preset, and
keeping it out of the toggle leaves ZapRail a plain enum instead of forcing it
into a data-carrying sealed interface.

Discovery needs the new <queries> entries: targetSdk is 37, so Android 11+
package visibility returns nothing from queryIntentActivities for an undeclared
scheme, and the chip would be invisible on every modern device. Unknown types
all fall back to payto://<type>/<authority>, so one payto entry covers the
open-ended tail of the vocabulary. Specific <intent> filters, never
QUERY_ALL_PACKAGES.

The mark is the resolved app's own icon, from the same ResolveInfo the probe
already holds, decoded once at the chip's size during the warm step and masked
round the way a launcher draws it. It falls back to the brand-coloured glyph
paymentTargetStyleFor already assigns when the hand-off would open a chooser or
merely a browser: https targets resolve to any browser, so a control probe
against an unownable host separates a real app handler from Chrome.

The availability cache is keyed on scheme plus host, not scheme, because an app
may declare host="iban" and a scheme-only hit would wrongly claim payto://upi
is handled. It is warmed from the sender's own target list when the picker
opens, so it is bounded by how many ways the user says they can be paid rather
than growing with the feed, and it is a StateFlow because a plain map write is
invisible to Compose.

Shared plumbing moves to commons: PaymentTargetTypes now owns the alias and
scheme tables that were duplicated inside the profile UI file, and
PayToRailMatcher holds the matching and the gate decision as pure functions,
free of Note, Context and the availability singleton so the gates are testable
on their own. RailCapability gains a defaulted payToTargets, and peek gains
defaulted parameters so zapClick's one-tap fast path stays Lightning-only.
PaymentTarget becomes a data class: without value equality it compares by
identity, which breaks list keys and dedupe.

25 new tests in commons; amethyst, commons and quartz suites all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXKZeV6FNhXF9BBjgEtfvS
2026-09-02 00:12:17 +00:00
..