Repository navigation
feat(chat): send back a DM's opening payment - #1220
Merged
Merged
Conversation
bmc08gt
force-pushed
the
feat/chat-send-back
branch
from
November 3, 2026 23:49
941c7d9 to
e0f29dd
Compare
…eturned payment ConversationMessage carries CashReturn from CashContent.payment_return, stored in two new message columns, so a reopened DM knows a payment was sent back without asking the server. Activity carries returnedPayment in a side table beside requestedPayment. Schema 64 -> 65.
The receiver of a cash message carrying a return offer gets a Send Back row under the bubble. A tap checks the balance at today's rate and opens a confirm sheet (amount, read-only currency, "Send back to <Name>", "Swipe to Send Back"), or the existing insufficient-balance modal. The swipe pays the original's amount, currency and mint back to the sender's owner under the server-issued intent id. The status is asked once per DM open, by the receiver only, and only when no message in the transcript returns the offer. The intent id is held in memory for the open and never logged; a failed return reports as "Send back failed" and shows "Couldn't Send Back". Both people see "Sent Back" on the original once the return is in the transcript, and the return reads "You sent back" / "<Name> sent back".
A payment that returned a DM's opening payment reads "Sent back to <Name>" to the one who returned it and "<Name> sent back" to the one it returned to, falling back to the server's title until the counterparty resolves. Tapping it opens the DM while it exists, else the details screen as other rows do.
…drop the row divider The footer kept its own copy of the card's 50% white, and the Available row drew an 8% white divider over a white fill, where it can't be seen. Neither has an app token. The footer now reads ChatCashCardCell.secondaryColor, and the divider is gone.
The balance check went through Session.hasSufficientFunds, which answers sufficient for a shortfall under half a denomination because its callers then send its amountToSend. Send Back pays the full priced amount, so a balance a fraction of a cent short passed the check and failed on chain. Compare held quarks with needed quarks instead, on tap and on swipe; the session check now only names the shortfall for the modal. A second tap while the first was pricing reported Send Back: Tapped twice and could open the sheet twice. Taps now wait for the one in flight. The sheet took the token's name from the viewer's balance, falling back to "Cash" with no icon when there was none. It now takes both from the mint's metadata.
Tapping a sent-back row looked the DM up in local storage only, so a DM not stored yet opened the details screen. It now goes through hydratedConversation, which fetches the chat from the server when it isn't stored, and opens details only when the server doesn't return it.
…er without animating The Sent Back footer takes the place of the card's bottom padding. A push that changes a card's Send Back state applies that card first, in place with animation off, so the bubble doesn't slide; the rest of the push keeps its spring.
With no fresh rate, or no loaded balance of the payment's mint, the tap and the swipe show Rate Unavailable and Send Back: Tapped doesn't fire, as on Android.
…ack context Needs a flipcash-shared-core-spm release carrying the catalogue change (code-android-app feat/analytics-send-back): until the pin moves, this commit does not compile against the published package. Sent Cash fires once per swipe; a return the server already counts as sent reports Failure with Error "Already Returned". No property carries the intent, chat or message id.
bmc08gt
force-pushed
the
feat/chat-send-back
branch
from
November 4, 2026 19:16
e0f29dd to
b2dbc8b
Compare
bmc08gt
marked this pull request as ready for review
November 4, 2026 19:50
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.
The receiver of a DM's opening payment can now send it back, once, from the bubble. The original sender gets the same fiat amount back in the same currency and mint, priced at today's rate, and both sides see the original marked "Sent Back".
Spec: orchestrator
docs/specs/send-back.md(orchestrator PR #111, 44c557f). Contract: flipcash2-client-protocol 0.25.0, already pinned here by #1218. This PR doesn't touch the pin or generated code. Design: section node 13296:476, frames S1–S3, S5, S6.Shared-core
flipcash-shared-core-spmis pinned to 0.20.0, which contains the Send Back catalogue change from code-payments/code-android-app#1952. The last commit sendsSend Back: TappedandSent Cashwith theSend Backcontext against it.What changed
Model and store.
ConversationMessagecarriesCashReturnfromCashContent.payment_return, persisted in two newconversation_messagecolumns, so a reopened DM knows a payment was returned without asking the server.ActivitycarriesreturnedPaymentin a side table besiderequestedPayment. Schema 64 → 65; the store rebuilds from sync as it does for any bump.The spec says to add
CashReturntoChatCashContent. That type is the rendered model, and the store persistsConversationMessage, so the rawCashReturnlives onConversationMessageandChatCashContentgets the derived row state (sendBack: .available / .sending / .sentBack).Row state.
SendBackRow.resolveis a pure function of the message, the viewer, the set of message IDs some loaded message'sreturn_ofnames, and the controller's local state:RETURNED.GetCashMessageReturnStatus.SendBackController. It lives onConversationLoadCoordinatorfor one DM open. It runs the status check once for the offered message when no loaded message already returns it, and keeps the server'spayment_intent_idin memory only. A failed check leaves the row hidden and shows nothing; the next open asks again.Tap → balance check → sheet or modal. On tap, it prices the original's
nativeAmountat today's rate in its currency and mint, and compares the quarks that needs with the quarks the receiver holds of the mint. The comparison is strict:Session.hasSufficientFundslets a shortfall under half a denomination through because its callers send itsamountToSend, and a return pays the full amount, so it's used only to name the shortfall for the modal. If no fresh rate is available, or no balance of the mint is loaded, the tap shows "Rate Unavailable" / "Couldn't get a fresh rate. Please try again." and opens nothing. If it's short,showYoureShortError(amount:)(orshowInsufficientBalanceError()when the shortfall isn't known) shows instead of the sheet, unchanged, with Add More Cash opening deposit for that mint. A tap while another is still pricing does nothing, soSend Back: Tappedfires once and the sheet opens once. The sheet's token name and icon come from the mint's metadata. The same check runs again on swipe; if it can't be made there, the sheet closes and Rate Unavailable shows. If it fails there, the sheet closes, the row goes back to Available and the modal shows. No intent is submitted.SendBackSheet. Built fromStartChattingSheet's parts:PartialSheet, the amount, a read-onlyCurrencyLabelpill with no chevron, "Send back to ", andSwipeControlreading "Swipe to Send Back".Submit. The submit path calls
Session.returnPayment(amount:verifiedState:to:chatID:messageID:intentID:)to the original sender's owner, resolved throughRecipientResolving. On failure it asks for the status once:RETURNEDbecomes Sent Back with no error; anything else returns the row to Available and shows "Couldn't Send Back" / "Something went wrong. Please try again." If the swipe's own status check already saysRETURNED, it goes to Sent Back with no payment.submitDirectreported every chat payment failure as "Requested payment failed"; a return now reports "Send back failed". That string only reaches Bugsnag;userFacingsets the report level and doesn't show a dialog.The intent ID is never logged, persisted, sent in analytics or put in an error report. The submit log line goes through one helper that prints
server-issuedin its place, and a test asserts that.Return bubble captions. A message with
return_ofreads "You sent back / to " to its sender and " sent back / to You" to its receiver, in the cash-bubbles line order, with no row or footer. A return that itself carries an offer follows the offered-bubble rules.Cell.
ChatCashCardCellgetsSendBackRowViewattached to the card's bottom edge: a white row with "Send Back" when Available, a spinner when Sending, and a caption-style "Sent Back" footer at the secondary text color. The Available and Sending row is 44 tall, below the card's 16 bottom padding. The Sent Back footer is 36 tall and takes the place of that padding, with its text centred. The card above keeps the #1221 spacing (16 / 12 / 6). The row has its own tap; tapping the card still opens currency info.No animation when the row or footer appears.
ChatViewController.update(items:)applies every push on a spring. When a push changes a cash card's Send Back state,sendBackStage(from:to:)swaps in only those cards, and they're applied first as an in-place reconfigure insideUIView.performWithoutAnimation. The rest of the push, such as the return message arriving with the sender's footer, then animates as before. The transcript can shift by the row's height once, with no slide. Cell height still comes from Auto Layout, and a reused cell drops the row.Activity. A row with
returnedPaymentis titled "Sent back to " (sent) or " sent back" (received), falling back to the server's title until the counterpart resolves. Tapping it opens the DM, fetching the chat from the server throughhydratedConversationwhen it isn't stored; when the server doesn't return it, or the fetch fails, the row opens the details screen, as on Android.Analytics (last commit).
Send Back: Tappedfires, withHas Balance, on each tap that reaches the sheet or the insufficient-balance modal. A tap whose balance can't be determined (no rate, or no loaded balance of the mint) doesn't count.Sent Cashfires once per swipe withContext= Send Back: Success; Failure withAlready Returnedwhen the status saysRETURNED, whether before or after a payment attempt; otherwise Failure with the error name, as other Sent Cash failures send it. NoRecipient Count, and no intent, chat or message ID in any property.Tests
Spec cases 1–19 are in
FlipcashTests/Chat/SendBackTests.swift(row, status gating, balance gate, submit outcomes, intent fields, analytics, intent ID kept out of logs) andFlipcashTests/ActivityRowReturnTests.swift(16, 17). Store round trips are inDatabase+ConversationsTestsandDatabase+ReturnedPaymentTests; row sizes (44, and 36 replacing the padding) and reuse are inChatCashCardCellTests; the no-animation staging is inChatSendBackStageTests.Design comparison
The App column comes from messages mapped through
ChatItem.fromand drawn by the realChatCashCardCellandSendBackSheeton an iOS simulator. The frames now use the cash-bubbles DM layout, so line order matches.appTextSmall(Bold), dark textLoadingView, used byCodeButton) isUIActivityIndicatorView, so the row uses that.StartChattingSheetuses a stroked capsule (Metrics.inputFieldStrokeColor), which this reuses.Token differences, using the app's tokens:
Color.textMainfill,Color.backgroundMaintext, in place of the frame's hard-coded white and near-black.appTextSmall(14 Bold).Color.rowSeparator(white at 10%), the colorRowdraws its separator in. The frame's line above the Available row is left out: over the white fill it doesn't show.ChatCashCardCell.secondaryColor, which is the 50% white the card's caption and counterpart lines already use..appTextCaptionandColor.textSecondary.StartChattingSheetuses, not the frame's values.