Repository navigation
feat(messenger): request money from a private group - #1851
Merged
Merged
Conversation
bmc08gt
force-pushed
the
feat/group-requests
branch
from
October 25, 2026 23:06
0aa7c48 to
26e1b0e
Compare
* feat(payments): split a group send's total between its recipients
Splits in the currency's minor unit, rounding each part down and giving
the leftover one unit each to the first recipients in selection order.
GroupSendSplitVectorsTest runs the shared test-vectors/group-send-split.json.
* feat(payments): pay a group send's parts in one intent
GroupSendPaymentDelegate prices each part from its own fiat amount at
one verified state and pays them with GroupPayment metadata: one
recipient as a single transfer, more as one multi-transfer in selection
order. The pricing and paying move out of ReactionBoostPaymentDelegate
into SharedStatePayment so both use the same path.
* docs(test-vectors): list group-send-split.json
* feat(messenger): pay checked members from a private group's amount sheet
A private group's amount sheet now leads to Select People and a confirm
step instead of posting a cash link. Checked members are paid in the order
they were checked, an 11th check shows the 10 People Max card, and a total
under one minor unit per person shows an alert and stays on Select People.
Public groups still post a cash link.
* feat(messenger): add Select People and the group send confirm sheet
* feat(chat): draw group sends as one bubble with a recipients sheet
A cash message with multi_receiver shows who sent the total, the per-person
amount ("~" on the smallest part when the split is uneven), how many people
it went to, and the viewer's own part when they were paid. Tapping it opens
a sheet listing the sender and recipients by name.
* fix(chat): credit and label only the viewer's own part of a group send
The eager balance update credited every non-sender with the full amount of
any cash message, so a group send would have credited each member the
total. It now credits the viewer's multi_receiver part, or the amount when
they are the single receiver, and nothing when the message names others.
The cash bubble said "You received" to every non-sender. A one-person group
send names its recipient, so other members now read "<sender> sent".
* fix(messenger): let the group send confirm sheet change the token
The token pill on the confirm sheet drew a chevron but ignored taps. It now
opens the token picker, and the send re-checks the balance, since the token
can change after the amount step checked it.
* fix(chat): loosen the group send bubble and details sheet
The details sheet stacked its rows 16dp apart with 24dp avatars, against
the design's 20dp rows and 32dp avatars, and had no top gap above Sent.
The group bubble now ends on a "to Alex Morgan and 2 Others" line, as on
iOS, and a group payment to one other member uses the same layout.
* fix(chat): show the group send token name at full opacity
* fix(messenger): match the confirm sheet token pill to other cash sheets
Use the White10 pill background and padding from the reaction boost sheet, and drop the Each line when only one person is paid.
* fix(chat): size the group send token row like the single send bubble
Use the single send bubble's token icon size and lowercase "each" in the per-person line.
* fix(chat): drop the group send "You received" line, name the viewer first
The per-person line already gives the viewer's share. When the viewer was paid, the footer reads "to You and N Others", matching iOS.
* fix(chat): trim the group send bubble's vertical padding
* fix(chat): show the viewer's avatar on the group send details sheet
* feat(messenger): open a recipient's profile from the group send details
Each To row now shows a chevron and opens that member's profile. The
push waits for the sheet's hide animation, since the sheet pops whatever
is on top once it settles.
* fix(messenger): show a group payment to you alone as the group card
* fix(messenger): list the payee of a one-recipient group payment in its details
* feat(messenger): pick the send currency from a half sheet in the chat send flow
Both the amount step's token pill and the group send confirm pill open
ChatStep.SendCurrency, which reuses the paid-reaction currency list, so
choosing a currency returns to the same step instead of leaving the flow.
* fix(messenger): open the sender and token from group send details
The From row and the Sent token row had no tap target, so an incoming
payment gave no way to reach the sender's profile. Both rows now show a
chevron: From opens the sender's profile, Sent opens the token info.
* fix(messenger): slide Select People in on the amount sheet's own nav host
The amount sheet swapped its content for Select People in place, so the
screen appeared without the forward slide. The sheet now hosts its own
FlowHost with Amount and People steps, driven by the view model's group
send stage, so Next and back animate like any other push and pop.
* fix(messenger): present group send like the share sheet
Group send opens as GroupSendAmountEntry, a FullscreenSheet like ShareMessage,
and its steps are NonDraggableRoute, so the amount and Select People screens
close by their X or back arrow only. The confirm sheet gets an X, no drag
handle, and turns off drag and tap-outside dismissal.
* fix(messenger): drop the confirm sheet's X to match iOS
The group send confirm closes by system back, which returns to Select People,
as iOS's back gesture does. Drag and tap-outside dismissal stay off for the
whole flow, and the amount step keeps its X. The X row gave the avatar its
top spacing, so the content now pads its own top.
… a version-merged store PaymentRequestController and its repository now return a PaymentRequest domain model instead of the generated paymentrequest.v1 types, and create takes a NewSplitRequest plus a 16-byte idempotency key. payment_intent_id travels only in PaymentRequestDetails, as a PaymentIntentId whose toString is redacted. PaymentRequests (shared/chat) opens a private group's request text once through GroupContentCrypto and keeps the greater version per request, EXPIRED over OPEN at an equal version. StatusLine and deckOrder carry the spec's time-left bands and deck order; unit tests cover the spec's cases 1-13.
directTransfer and IntentTransfer.create take an optional intentId; without one they mint a fresh id as before. Submitting twice under the same id lands once, so a retry or a second tap cannot pay a share twice. PaymentRequestPaymentDelegate.prepare refetches the request and returns Ready only for an open request with an unpaid share and an issued payment_intent_id; pay sends that share to the creator in the request's currency with requested-payment app metadata, then refetches. The id stays in memory inside ShareToPay.Ready, whose toString leaves it out.
…orage and preview A payment request widget now decodes to WidgetContent.PaymentRequest instead of unsupported, encodes for sending, and survives the message row. The chat list previews it as "<name> requested <share>" from the request store; one the device doesn't hold yet previews as nothing. The bubble still draws as unsupported until the request card lands.
…t message A failed send leaves the pending row that the transcript already retries, so a created request is posted again without creating it twice.
…hares Shares go in roster order with the creator first, a group over 100 members starts with nobody selected, and a preset deadline resolves when Send is tapped. The request's idempotency key now uses the chat IdempotencyKey type.
…up's cash button Request Money skips the balance gate, since asking for money spends nothing. The Money sheet reuses Add Money's card actions, now public in :shared:funding.
Request Money opens on the group's request history; New Request walks amount per person, a summary with name, people and time-to-pay editors, and a confirm sheet. Send Request creates the request once per confirm, under one idempotency key, then posts the request widget as a pending chat message, so a failed send retries the message and never re-creates the request. Select People takes its button text and shows the viewer as "You" so the request flow reuses it. The Money sheet change in f8a3308ec stopped four group-send tests from reaching Select People; they now pick Send Money from the sheet.
The card resolves from the store through a pure requestCardState: a request from another chat or creator draws as unsupported, and a DENIED or NOT_FOUND fetch marks the id missing so the card stops refetching.
…cards in the transcript
… marking shares paid
The amount keypad had no currency symbol until the user typed, the expanded deck and pill cards let the transcript show through, and the summary and details screens used divider rows where the design has cards.
- The Money sheet has two rows, Send Money and Request Money, with no title or Dismiss row. - Create and details-action failures that have no specific copy show the app's network error instead of a new generic string. - Send is hidden on request details when the fetch carried no payment intent id for the viewer (case 20). - A request's widget message is retried twice on the same pending row, then shows the chat's Message Not Sent error with a retry. Create is never called again. - Request copy uses ' rather than ’, as the spec does. - Tests cover cases 11 and 12 through the store merge, the summary copy for 14 to 18, and case 22's "24 hours left to pay".
The pay delegate holds the server's payment_intent_id in LiveSecrets before it submits, so the SubmitIntent body and trace show only its last 6 characters. A failed payment logs that suffix and keeps the id held for the retry. A successful one releases the id and logs it in full.
Request Money swapped its content in place between history, amount, summary and the editors, so none of them slid in. Each stage is now a step on the screen's own FlowHost, the way group send hosts Select People since dc3692f. The view model's stage still decides which step is on top, and back goes through the view model so the stack follows the stage. The deadline and confirm sheets stay drawn over Summary.
…ets with a close Both now open the way the share sheet does: a fullscreen sheet whose root shows a close X. Drag-to-dismiss is off on both, so the close is the only way out and a half-made request can't be swiped away.
… the share sheet does Both are sheets now, so the history root's back arrow becomes the share sheet's close at the end of the app bar. Request details keeps its ••• beside the close.
…ce, push details from history Expanded deck: tapping the scrim collapses it. Title, ring, amounts and status are each their own shared element between the pill and the first expanded card. Request details' options use the haze circle button. Deadline date and time pickers take the app's colors and type. A history row pushes Request details on Request Money's own host.
The request's Name step now renders through NameEditScaffold, the layout EditGroupNameScreen uses, so both edits share the heading, field, counter and keyboard handling. Done stays the request's button.
…kdrop The expanded deck dimmed the chat with its own scrim, which stopped at the app bar. It now uses the blur and dim the long-press message menu uses, over the whole screen, the back button, avatar and title included. Tapping anywhere outside the cards collapses, including the gaps between them. The backdrop was hand-copied in three places (message rows, their reaction pills, the composer). It moves into a shared chatBackdrop modifier with the same 8dp blur and 0.4 alpha; the long-press menu should look and behave as before.
Opening a card from the expanded deck collapsed the deck first, so the collapse ran behind the details screen as it came up. The deck now stays expanded, and back from details returns to the list as it was. The expanded deck drops its grab handle; drag up still collapses it. A close button takes the collapsed pill's place in the app bar, above the backdrop. It fades in with the backdrop and out first on collapse, so it is gone before the pill lands where it sits.
…sule Request details showed "•••" and X as two separate circles; iOS shows them in one capsule. ActionCapsule in chat-ui has the photo viewer's pill shape, surface and spacing (#1854), so the viewer can switch to it once both land. StripSurface becomes internal for it, as #1854 also does. The deck's close X uses the same capsule; its placement is unchanged.
bmc08gt
force-pushed
the
feat/group-requests
branch
from
October 26, 2026 01:35
7ccc636 to
dac2a03
Compare
Opening a request from the expanded deck left the deck expanded under the sheet, so back returned to the expanded list. The Request details step now waits for its sheet to be fully up, then sets the deck minimized while it is hidden. Back shows the chat with the deck in the app bar and no collapse animation as the sheet leaves. ChatRequestsViewModel moves to the flow's store so the step can reach the chat's instance.
…deck The card behind now moves up with the front card as it is swiped and shows its request over the second half of the swipe, so the deck lands on a filled card instead of an empty slot. The swiped card fades in at the back, the pill eases between title lengths, and the press ripple is clipped to the pill.
The mapping could word a request's preview, but no list passed it the requests, so a chat whose latest message is a request previewed as nothing. The Chats and Archived lists now read the requests store and fetch a previewed request the device doesn't hold yet, once, so the row fills in on a cold start without opening the chat.
The chat list previewed a request as "You requested $5.00". It now reads "You sent request for Dinner" or "Alice sent request for Dinner", with no amount. A title that can't be opened drops the "for" part.
animateContentSize measured the pill's text at each in-between width, so a longer title arriving in front read "Sund…" until the pill caught up. The title and amounts now measure up to the widest pill's text room and the easing pill clips them, so a title ellipsizes only when it is too long for that width.
The front card now stops at one pill height and rubber-bands past it. The card behind scales up into the front slot as you drag and shows its request the whole way, so the pill is never empty. On release the swiped card drops behind and springs into the back slot instead of fading in there. Release and spring-back are springs, so a new drag picks the deck up where it is.
The pill held the old request's width through the hand-over, then jumped to the new one, so a longer title showed clipped and a shorter one left a gap. The deck width is now its own spring, started when the swipe is let go and run on the cards' spring, so every card stretches with it as on iOS. Release also hands over at once: the swiped card and the cards behind finish their move from where the finger left them instead of first carrying the swipe on to a full pill height.
While the viewer has an unpaid share in an open request, the deck leaves the app bar for a full-width card under it: title, "You owe <share> · <time left>" and a Pay button that opens Request details straight into paying. Scrolled away from the newest messages, it shrinks to a green app-bar pill instead. Requests the viewer owes on sort to the front. The first time this device sees an owed request, the deck expands by itself with a NEW chip on that card and collapses after 2.5 s. It skips this with a screen reader or reduced motion. The pill, green pill and strip draw one stack, so they swipe the same way. The compact pill toggles when the newest message leaves the screen rather than tracking the scroll.
…xpand Scrolled away from the newest messages, the strip now shrinks into the normal grey app-bar pill instead of a green one, and grows back at the newest messages. The deck no longer opens itself the first time it sees an owed request: the NEW chip and the per-device store of shown requests are gone. The owed screen-reader label is now read in the strip only.
Scrolling away from the newest messages shrinks the owed strip into the grey app-bar pill as the finger moves; a scroll released partway settles to the nearer end. Once it reaches the pill it stays there for the rest of the chat visit, so scrolling back to the newest messages no longer grows it. The strip comes back when the chat is reopened while still owing, or when a new owed request arrives while it is open. The pill, strip and expanded deck now share one seekable transition, so the front card's shared bounds can morph between any two of them. The strip consumes the scroll while it shrinks, so the timeline doesn't jump. DeckRest holds the pinning rule and replaces deckPlacement.
Each deck card now says where the viewer stands on it, so a member can tell which requests they've paid without opening each one (spec "Viewer status", Figma row G). - Paid share: a success badge with a black check at the glyph's bottom-right, ringed in the card surface, and "✓ You paid $5". - Owed share: "You owe $5" in green. Creator: "Your request" in grey. No share: nothing extra. - Pill and strip back cards: the standing leads the line and the " · $15 of $45" totals drop when the line doesn't fit. - Expanded cards keep "$15 paid, $45 requested" on the amounts line and put the standing at the footer's trailing end. - The deck's screen reader label adds the standing after the title. ViewerStanding is resolved in the request model; tests 29-32 cover paid, owed, creator and no share.
Replaces the six-row deadline list and the separate date-and-time picker with one sheet: preset chips from 1 Day to 4 Weeks, a grid of today through today + 28 days, and a Time row that appears for a custom day. The custom time defaults to 6:00 PM, is clamped to now + 1 h through now + 28 d - 5 min, and opens Material 3's time picker dialog. Spec tests 34-39 are in TimeToPayTest.
The readout above the calendar becomes two fields: Starts reads "Today, on send", and Closes shows the deadline with a white outline and is announced live. A summary line under them names the preset or the custom day count. The calendar marks today with a grey circle and the closing day with a white one, joined by a 12% white band that breaks at each week's edge. Picking today shows the white circle alone. The Closes value shrinks to fit instead of cutting off a custom day and time on a phone-width field.
… selection Time To Pay now always shows the Time row. A preset reads the current time of day; changing it makes the preset's closing day custom at that time, clamped as before. A tapped day keeps the last time set. The chip highlight, the closing-day circle and the range band now move to a new choice. The band draws as one span per week row so it has no seams while it moves. With animations off they jump straight there. Spec tests 34 and 38 now expect the Time row; 41-45 are new.
…n it While the closing-day circle slid across the grid, each day it passed turned black for a frame before the circle reached it. The text colour now blends from white to black over the last quarter cell of the circle's approach. A band span that had just started growing into a row drew as a thin capsule at the row's edge. Spans narrower than the band is tall now fade in with their width.
The Time To Pay grid stacks the abbreviated month (8sp semibold, uppercased) above the number on the 1st, centred as one group in the 34dp circle. The label colour follows the cell's state: dimmed, inside the band, closing day, or start circle (tests 46-47).
A press on a Time To Pay day lit the whole 38 dp cell, a square that ran into the neighbouring cells. The ripple is now a 34 dp circle centred on the cell, the size of the selected circle; the whole cell still takes the tap.
…ircles Each band span drew with a full rounded cap, so where it met the start or closing-day circle the cap pinched inward under the circle's edge. A span now rounds only the ends where the range carries on past the row edge, and stays square where it starts or ends at a circle, including under the moving circle while it slides. Test 48 covers the per-end rounding.
…s cards The flow host got the current stage's back stack as its initial stack on every recomposition. Until the user has moved off the start, the host treats a new initial stack as a correction and swaps it in without a transition, so History -> amount appeared instantly while later pushes slid. The initial stack is now remembered once. The history list now uses iOS's card rows: 12dp corners on a 6% white fill, 16dp padding, title and date on one baseline, amounts below, and a chevron. The list gets a 20dp inset and 12dp between items.
bmc08gt
marked this pull request as ready for review
October 26, 2026 19:31
* commit '5f2549710405c4215ac47fc972e0c3d22f4e8dfb': feat(history): show boosted reactions in transaction history (#1858) feat(chat): in-app alert for reactions and boosts on your messages (#1857) feat(chat): tap-to-show photo viewer chrome with react, reply, save and forward (#1854) # Conflicts: # apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt
This was referenced Oct 26, 2026
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.
Adds group payment requests (splits) to private group chats, following the spec in code-payments/flipcash-client-orchestrator#63. It builds on #1846 for Select People, so this PR targets
feat/group-send.Cipher. The request title and description are encrypted with the group key in
:kmp:shared-core'sDefaultChatCipher(c572f6e), so iOS uses the same code. It passes all 4 encrypt/decrypt vectors and all 5 must-fail cases inpayment_request_text.json.Model and store.
PaymentRequest/Split/Sharereplace the raw protos in the service, repository and controller.PaymentRequestsinshared/chatmerges by version, with EXPIRED beating OPEN at equal versions. It loads OPEN/100 and RESOLVED/20 per chat, pages history, and keeps a missing marker so a DENIED/NOT_FOUND card stops refetching. Logout clears it.Paying.
directTransfertakes an optional intent id.PaymentRequestPaymentDelegatepays a share under the server'spayment_intent_idand retries under that same id. The id stays in memory and is never logged.Chat. The payment request widget gets decoded, sealed inside
EncryptedContent, stored in Room and previewed in the chat list ("X requested $20", using the viewer's share). Sending it goes through the existing pending-message path, so a failed send retries the message, not Create.Create. In a private group the cash button opens a Money sheet with Send Money and Request Money. Request Money goes history → amount per person → summary (people, deadline, name) → confirm. Shares follow roster order with the creator first. Groups over 100 members start with no one selected. The deadline is resolved when Send Request is tapped, and the idempotency key is generated once per confirm sheet.
Viewing. The request card shows in the transcript. The app bar shows a deck: swipe down cycles it, there's a "Next request" accessibility action, and tapping expands it into the list with a shared-element transition. Request details covers paying your share, plus close and mark-paid for the creator. Requests refresh every 45 s while the chat is visible and on new widget or cash messages. Ended requests stay greyed for 24 h.
Editing a request is not exposed; the server doesn't support it yet.
Spec cases 1–20 are covered by unit tests in
shared/chat,shared/paymentsandfeatures/messenger; case 21 (the vectors) is in:kmp:shared-core.