Skip to content

feat(payments): send an explicit action on every tip DM payment - #1442

Merged
bmc08gt merged 1 commit into
code/cashfrom
feat/intent-payment-action
Sep 11, 2026
Merged

bmc08gt merged 1 commit into
code/cashfrom
feat/intent-payment-action

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

flipcash2-client-protocol 0.5.0 adds action to
intent.v1.ChatMetadata.TipDmPayment. This pins 0.5.0 and sets the field. The
iOS side is code-payments/code-ios-app#752.

Who decides the verb

The recipient's "You tipped" / "You sent" comes from messaging.v1.CashContent.verb,
which the server writes. Neither client ever sets it — iOS reads it once, at
ConversationMessage.swift:163-169; Android reads it at ProtobufToLocal.kt:159-170,
and the one setVerb it has (LocalToProtobuf.kt:127-141) is unreachable because
nothing builds outbound CashContent.

Server side that resolution is intent.GetDmPaymentVerb (flipcash2-server,
intent/chat.go:117-138). It prefers action, and falls back to location only when
action is DEFAULT. location has exactly one reader in the whole server — that
fallback. action has exactly one reader — the switch above it.

The resolved verb drives three things, which is why it is worth getting right:

  • the cash message injected into the DM (task/chat.go:89),
  • the sender's activity feed title (activity/localization.go:30),
  • and the tip DM validation rules (intent/chat.go:258).

action is not optional here

Validation keys off the same verb (validateTipDmAppMetadata, intent/chat.go:252-308):
a tip may target a chat that does not exist yet and must clear the per-currency
minimum plus the recipient's initialization fee; a send has no minimum but is denied
with "tip dm has not been initialized" unless the chat is already there.

So the payment that opens a tip DM must resolve to TIPPED. Sending action = SEND
on that path would be rejected outright. This is the constraint both clients had
already discovered from the outside and recorded imprecisely as a rule about
location (SendAmountViewModel.swift:86-95, ChatViewModel.kt:1073).

What each app sends

Situation location action
Payment from a scanned or linked tip card TIPCARD TIP
The payment that opens the tip DM TIPCARD TIP
Send Cash inside an initialized tip DM CHAT SEND
Contact (phone) DM payment n/a n/a

Same rule on both platforms. ContactDmPayment has no action field.

DEFAULT is never sent

Each app models the action locally with two values, so DEFAULT is unrepresentable
rather than merely avoided. Location's zero value is TIPCARD, not an unknown, so a
message that leaves action unset is indistinguishable on the wire from one
deliberately declaring a tip — the server's own comment at intent/chat.go:130-133
spells that out. Leaving the field at DEFAULT hands the verb back to the inference
this field exists to replace.

location does not move

Not one location byte changes on any path, and that is deliberate: it is the
compatibility path. Server support for action landed on 2026-09-09
(flipcash2-server#196), one day before flipcash2-client-protocol 0.5.0 was cut. A
server without that commit reads location alone.

Because action is set to exactly the verb location already implied, both server
versions resolve the same verb, so these clients are correct against either. Make
location honest at the same time — send CHAT for a chat-composed payment that
opens the DM — and the two versions disagree: the new one allows it, the old one
denies it as an uninitialized send. That cleanup is worth doing once #196 is
confirmed deployed everywhere, and it is a one-line change on each platform then.

On Android specifically

TipAction sits next to TipOrigin in DmPaymentMetadata.kt and
buildTipDmPaymentMetadata now requires it, so no call site can forget it.
TipPaymentDelegate.send takes it through. TippingCoordinator passes TIP —
a tip card has no send path.

ChatViewModel computed isTip and used it for two things that had drifted
apart in wording: picking the location and picking the analytics event. It now
computes one tipAction and all three read off it — the wire field, the
location, and the event.

Not fixed here

The "Swipe to Tip" label and the tip fee floor come from
openingTipRecipientFlow, which decides tip-ness from isChatInitialized —
chat members observed from the server — rather than from typingConstraints
like the send handler and the button label do. Those two read the same
expression on the same state and cannot disagree with each other; this one can
disagree with both, because it depends on a server round trip they don't wait
for. So the swipe label can promise a tip while the payment declares a send.

This change puts the wire field on the send handler's side rather than adding a
fourth predicate, but the split is still there.

`flipcash2-client-protocol` 0.5.0 adds `action` to
`intent.v1.ChatMetadata.TipDmPayment`, and the server prefers it over
`location` when it resolves the verb the recipient sees ("Tipped" vs
"Sent"). Left unset it reads as `DEFAULT`, which falls back to the
location — and since `TIPCARD` is also the zero value, an unset action
on an unset location resolves to a tip. So the field is not optional on
the path that opens a tip DM: the server denies that intent unless it
resolves to a tip.

Set it everywhere a `TipDmPayment` is built. `TippingCoordinator`
always sends `TIP`, since a tip card has no send-cash path.
`ChatViewModel`'s send handler derives one `tipAction` and uses it for
both the wire field and the `SentTip`/`SentCash` analytics event,
instead of the event re-deriving tip-ness on its own.

`TipAction` carries only `SEND` and `TIP`, so `DEFAULT` is
unrepresentable rather than merely avoided.

`location` keeps its current values on every path. A server predating
the field reads `location` alone, and because `action` agrees with what
`location` already implied, both server versions resolve the same verb
through the rollout.

`buildDmPaymentMetadata` (contact DMs) is unchanged; `ContactDmPayment`
has no action field.

Bumps `flipcash2-client-protocol` 0.4.1 to 0.5.0 for the new field.
@bmc08gt bmc08gt self-assigned this Sep 10, 2026
@github-actions github-actions Bot added type: feature New functionality area: payments Payments, transfers, intents, billing area: network gRPC, connectivity, API, exchange rates area: build-system Gradle, convention plugins, build-logic and removed type: feature New functionality labels Sep 10, 2026
@bmc08gt
bmc08gt merged commit 57e675c into code/cash Sep 11, 2026
4 checks passed
@bmc08gt
bmc08gt deleted the feat/intent-payment-action branch September 23, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: build-system Gradle, convention plugins, build-logic area: network gRPC, connectivity, API, exchange rates area: payments Payments, transfers, intents, billing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant