Repository navigation
feat(payments): send an explicit action on every tip DM payment - #1442
Merged
Merged
Conversation
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
flipcash2-client-protocol0.5.0 addsactiontointent.v1.ChatMetadata.TipDmPayment. This pins 0.5.0 and sets the field. TheiOS 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 atProtobufToLocal.kt:159-170,and the one
setVerbit has (LocalToProtobuf.kt:127-141) is unreachable becausenothing builds outbound
CashContent.Server side that resolution is
intent.GetDmPaymentVerb(flipcash2-server,intent/chat.go:117-138). It prefersaction, and falls back tolocationonly whenactionisDEFAULT.locationhas exactly one reader in the whole server — thatfallback.
actionhas exactly one reader — the switch above it.The resolved verb drives three things, which is why it is worth getting right:
task/chat.go:89),activity/localization.go:30),intent/chat.go:258).actionis not optional hereValidation 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. Sendingaction = SENDon 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
locationactionTIPCARDTIPTIPCARDTIPCHATSENDSame rule on both platforms.
ContactDmPaymenthas noactionfield.DEFAULTis never sentEach app models the action locally with two values, so
DEFAULTis unrepresentablerather than merely avoided.
Location's zero value isTIPCARD, not an unknown, so amessage that leaves
actionunset is indistinguishable on the wire from onedeliberately declaring a tip — the server's own comment at
intent/chat.go:130-133spells that out. Leaving the field at
DEFAULThands the verb back to the inferencethis field exists to replace.
locationdoes not moveNot one
locationbyte changes on any path, and that is deliberate: it is thecompatibility path. Server support for
actionlanded on 2026-09-09(
flipcash2-server#196), one day beforeflipcash2-client-protocol0.5.0 was cut. Aserver without that commit reads
locationalone.Because
actionis set to exactly the verblocationalready implied, both serverversions resolve the same verb, so these clients are correct against either. Make
locationhonest at the same time — sendCHATfor a chat-composed payment thatopens 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
#196isconfirmed deployed everywhere, and it is a one-line change on each platform then.
On Android specifically
TipActionsits next toTipOrigininDmPaymentMetadata.ktandbuildTipDmPaymentMetadatanow requires it, so no call site can forget it.TipPaymentDelegate.sendtakes it through.TippingCoordinatorpassesTIP—a tip card has no send path.
ChatViewModelcomputedisTipand used it for two things that had driftedapart in wording: picking the
locationand picking the analytics event. It nowcomputes one
tipActionand all three read off it — the wire field, thelocation, and the event.
Not fixed here
The "Swipe to Tip" label and the tip fee floor come from
openingTipRecipientFlow, which decides tip-ness fromisChatInitialized—chat members observed from the server — rather than from
typingConstraintslike 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.