Skip to content

feat(chat): declare the payment action on tip DM payments - #752

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

feat(chat): declare the payment action on tip DM payments#752
bmc08gt merged 1 commit into
mainfrom
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
Android 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 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 iOS specifically

ChatPaymentMetadata.tipDm gains an action, and a new TipDmAction with two
cases serializes it. SendAmountViewModel picks the action from the origin it
already 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 sentTip regardless of what the
wire said. It now reads ChatPaymentMetadata.isTip, which is the action being
sent.

Two doc comments described the old inference as the server's rule. They now say
what the server actually does with each field.

`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 bmc08gt self-assigned this Sep 10, 2026
@bmc08gt
bmc08gt merged commit 8052161 into main Sep 11, 2026
1 check passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant