feat(chat): declare the payment action on tip DM payments - #752
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 (`SENT` vs `TIPPED`). 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. A tip card payment and the payment that opens the DM send `TIP`; Send Cash inside an already-initialized tip DM sends `SEND`. `TipDmAction` carries only those two cases, 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. Analytics on the send screen now reads the same metadata the wire carries instead of re-deriving tip-ness, so the event and the verb cannot drift.
bmc08gt
added a commit
that referenced
this pull request
Sep 11, 2026
…discrete-curve * origin/main: (27 commits) fix(database): share one SQLite writer per owner and take write locks up front (#759) feat(chat): declare the payment action on tip DM payments (#752) refactor(chat): drop the deprecated new_messages overlay (#757) feat(notifications): write prefetched messages into the shared store (#756) refactor(store): move the persistence layer into a shared FlipcashStore package (#755) feat(database): move the SQLite store into the App Group container (#754) feat(database): open the store on demand, close it on background (#753) feat(nse): extension crash reporting, a WAL checkpoint, and on-device push hooks (#751) feat(home): long-press the You tab to open the account switcher (#749) fix(tests): reset Photos access before the previous app instance lingers (#746) chore: bump version to 2026.9.2 (#745) revert: back out the Coinbase Stable Swapper authority migration (#747) (#750) fix(swap): follow the Coinbase Stable Swapper authority migration (#747) fix(tests): cancel a cash link through the details screen (#744) fix(chat): make the whole Send Cash pill tappable while it stands alone (#743) fix(username): drop a leading @ in the validator (#742) fix(chat): scope the send-button spring to the button (#741) fix(transactions): tighten the details card stack and drop the header badge (#740) fix(transactions): draw View in Chat as a card, not the primary action (#739) feat(chat): flash the message a reply-quote jump lands on (#738) ... # Conflicts: # Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved # FlipcashCore/Package.swift
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. TheAndroid side is code-payments/code-android-app#1442.
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 iOS specifically
ChatPaymentMetadata.tipDmgains anaction, and a newTipDmActionwith twocases serializes it.
SendAmountViewModelpicks the action from the origin italready computes, so the two cannot disagree.
The tip analytics event moves onto the same value. It previously re-derived
tip-ness from the recipient case, which meant the scanned tip card and the Send
Cash action inside a tip thread both reported
sentTipregardless of what thewire said. It now reads
ChatPaymentMetadata.isTip, which is theactionbeingsent.
Two doc comments described the old inference as the server's rule. They now say
what the server actually does with each field.