Repository navigation
feat(group-requests): ask a private group for money and pay a share - #1129
Merged
Merged
Conversation
bmc08gt
force-pushed
the
feat/group-requests
branch
from
October 25, 2026 15:25
b8155da to
61f5097
Compare
Adds group payment requests from the shared spec: the create flow behind the chat's money button, the request card in the transcript, the app-bar deck, request details with close and mark-paid for the creator, Request Money history, and paying a share under the server's payment intent ID. The request text is sealed and opened through shared-core's encryptPaymentRequestText / decryptPaymentRequestText. Until a shared-core release carries Android 62ac41455, the branch builds only with FLIPCASH_SHARED_CORE_LOCAL pointing at a local XCFramework. Database.schemaVersion is bumped for the new widget content.
…send failure - Back collapses an expanded deck instead of leaving the chat; the rear cards rise from beneath the front card. - Request details refetch every 45 s while on screen. - A request message that fails to send stays in the chat as a failed row with retry, like any message, instead of an alert that pops the flow. A successful send now refreshes the open transcript. - "Choose People" only for an empty selection; custom timing shows the year; the custom picker's bounds are taken when it opens. - Chat list preview reads "<first name> requested <amount>" or "You requested <amount>", fetching the request when the list needs it. - Test for spec case 22: ends_at = now + 24 h reads "24 hours left to pay".
…ke Share Request Money, request history's details, and the chat's request details now open with fullScreenCover, the presentation ShareToChatsSheet uses. Swipe-down no longer dismisses them; the close X in each toolbar does.
…istory details - Group send in a private group opens full screen with its close button and no swipe-down, through a new `coversFullScreen` flag on the router's sheet presentations. - The app-bar pill follows concept B: the front card with up to two more peeking beneath it. - Expanded deck cards sit on the opaque card surface instead of glass. - The container, ring, title and amounts each move between the pill and the front expanded card; the text moves without resizing so it does not truncate mid-flight. - Tapping the dimmed area below the expanded cards collapses the deck. - The custom deadline picker uses the wheel style. - Tapping a request in Request Money's history pushes its details onto the flow's stack instead of presenting them.
Tapping Name now pushes a screen laid out like the group-name and profile-name screens: a heading, one large field, a countdown near the 100-scalar cap, and a filled Done button. It replaces the half-height sheet.
The disc behind the "$" now takes the ring's accent, success green while the request is open and secondary grey once it ends, with a black glyph. Android's RequestRing draws it the same way.
…e spec The private-group money sheet is now two Add Money-style cards with a trailing withdraw/deposit glyph and no title or Dismiss, as Android draws it (node 12243:1009). The request ring is a 2 pt stroke on the row-separator track, the disc sits 4 pt inside at 26 pt (scaled for larger rings), and a closed request shows a check in place of the dollar sign, matching Android's RequestRing.
…sabled A row with no mark-paid action was a Button with .disabled(true), which dims the names and amounts with the rest of the row. Those rows are now a plain view; only rows you can act on stay buttons.
The root's softScrollEdge doesn't reach a full-screen cover, so request details sets it on its own scroll view, as ShareToChatsSheet does.
Card-only lines (progress bar, deadline, Request label) belonged to no container while expanding, so they showed at full opacity over the chat before the card formed. One animated progress value now drives the front card from the pill's measured frame to the card's, and those lines fade in from 60% of the move and fade out first on collapse. The pill keeps its final layout throughout, so it lands without re-measuring, and any tap mid-move reverses from where it is.
The toolbar keeps the pill's text at one size while the overlay follows the Dynamic Type setting, so at a smaller text size the moving copy drew the title and amounts about 9% smaller than the pill. When the overlay left, the pill's own text replaced it at its larger size. The moving copy now draws the pill's text at the pill's type size.
When the first fetch or a new request lands after the chat is on screen, the pill fades in over 250 ms from 95% scale while the title widens its margin in the same animation. It fades out the same way when nothing is left. A chat that opens with requests already known shows the pill from the first frame, and a refetch that keeps it showing doesn't replay the fade.
… message menu The expanded deck dimmed the chat with its own fade and left the app bar's back button, avatar and title showing. It now draws the long-press menu's ChatBackdrop (same blur fraction and dim) through a SwiftUI wrapper, in a window above the navigation bar, so the backdrop covers the whole screen and only the moving and expanded cards sit above it. The backdrop's opacity follows the deck's progress, so it comes and goes at the expand/collapse pace, and a tap on it anywhere outside the cards collapses. The moving card stays mounted and is hidden at rest. Inserted in the same update that starts a collapse, it had no starting value and jumped straight to the pill.
…d close it from where the pill sits The expanded deck drops its grab handle and shows a glass close button where the pill sits. Opening a card's details now presents them from the deck's window, so the deck no longer collapses underneath and going back finds it as it was.
…from it Once details finish covering the chat, the deck snaps back to the pill with no animation, so closing details shows the chat with the deck minimized and no backdrop. Deck order is unchanged.
… app bar Opening a chat with an open request hung at 100% CPU: the peeking cards took their width from the front card's measured size inside the same stack, so a peek could outgrow a front card the bar had just narrowed and the bar re-measured the pill every frame. The peeks now sit in a background that takes the front card's size and adds none of its own.
…n preview them
A request message is sealed, but ChatSeal.readable returned nil for every
widget, so each one opened as unsupported and stayed ciphertext. The chat
list row had nothing to preview ("You requested $1.00" never showed), and a
request from another member drew as "This Message Needs a Newer Version of
Flipcash" in the transcript.
A sealed widget now opens when it is a payment request with an id. A sealed
profile share still opens as unsupported, since only the server sends one.
schemaVersion goes to 58 so stores that already hold these rows as
ciphertext rebuild from the server.
The row now reads "<first name> sent request for <title>" or "You sent request for <title>". The title is opened with the chat's seal, so the row loads the seal alongside the request. A title that hasn't loaded or won't open drops the "for <title>" part instead of blanking the line.
Dragging the pill down moved one reused face into the chat, past the first message, and left the pill empty; the next request only appeared after release, and the ring animated apart from its card as the face swapped contents. Each request is now its own card, keyed by id. The front card follows the finger and rubber-bands at one pill height while the card behind rises and grows into the front slot. Past the threshold the front card drops behind into the back slot; short of it, everything springs back. The pill's size still comes from the front request at rest, so the moving cards never feed back into the bar's layout.
While the viewer has an unpaid share in an open request, the deck leaves the app bar and sits under it as a full-width strip: "You owe <share> · <time left>" with a Pay button that starts the existing pay flow. Swipe down cycles as in the pill, and tap expands from the strip. - Deck order puts open requests the viewer owes on first, by nearest deadline, then other open requests, then ended ones. The creator never counts as owing. - Scrolled away from the newest messages, the strip shrinks into a green app-bar pill and grows back on return. - The deck opens itself once per owed request per device, newest first with a NEW chip, and collapses after 2.5 s or on tap. It skips this under VoiceOver or Reduce Motion. - Each collapsed face records its own frames, so a face fading out can't overwrite the one the expand morph lands on. - The pay sheet moved out of request details so the strip can open it. Spec: group-requests.md "When the viewer owes", tests 23-26.
…ind the strip Behind the owed strip the app-bar pill is transparent but still takes its slot, so a title sized without it ran under the back button.
…op auto-expand While scrolled away from the newest messages, the owed strip now shrinks into the normal app-bar pill instead of a green one, and grows back at the newest. The deck no longer opens itself for a new owed request, so the NEW chip and the per-device record of which requests it opened for are gone. Only the strip reads the owed accessibility label.
Details opened from the expanded deck live in the deck's own window, which doesn't receive the app's scene phase, so it read as background. The details fetch only runs while active, so the pay intent never loaded and Send never showed. The deck window now passes the app's scene phase to the details.
…howing loading A request card, the deck and Request details stay in their loading state until the chat's seal opens the encrypted title. The chat and details read the seal once on open and never again, so one failed read (a slow getChat, a key not yet synced) left them loading until the app came back to the foreground. The store now reads a missing seal on every chat refresh, alongside the request pages instead of before them, and after each single-request read, so the 45 s poll retries it. Concurrent reads for one chat collapse into one.
…ls open over it Opening a back card's details snapped the deck to the pill as soon as the details finished appearing, which showed through the sheet as it slid up. The snap now happens as the details start to close, while they still cover the screen, so the deck underneath never moves on open and is already the pill on the way back.
…at the next 45 s poll A chat card or Request details left loading by a failed section read, a failed GetPaymentRequest, or a chat key that wasn't ready waited for the next 45 s poll, and a failed single-request read outside the first pages was never read again. The store now remembers failed reads, retries them on the next chat refetch, and reports whether a chat or request is still settling; both polls then retry after 2 s, doubling up to 45 s.
… the scroll The strip swapped for the pill on the jump button's at-bottom flag, so it popped instead of tracking the scroll, flapped on small scrolls around the threshold, and inserting or removing it moved the transcript's top inset by its height. The transcript now reports how far the reader sits above the newest messages on every scroll. The strip stays mounted while the viewer owes, so the inset holds, and scales, lifts and fades toward the pill's slot over the first 140 points while the pill fades in. The style flips at 65% going away and 35% coming back.
…ttle a half scroll Scrolling away from the newest messages faded the strip out while the grey pill faded in, so the back cards, glyph and text moved as two layers. The strip's stack now interpolates from the strip slot to the pill's frame as one piece and the pill takes over only at the end. Releasing a scroll partway through the collapse left the strip half shrunk. The chat now settles the scroll to whichever end is nearer: back to the newest messages with the strip open, or past the collapse distance with the pill pinned.
Username.handle already starts with "@", and the sheet prefixed another.
…as fully collapsed Scrolling back to the newest messages no longer grows it back. A new owed request arriving while the chat is open brings the strip back, and a reopened chat starts with it out.
A paid badge on the glyph and a status line ("✓ You paid $5 · $15 of
$45", "You owe $5 · …", "Your request · …") replace the bare amounts in
the pill, the strip's other cards and the expanded list. The totals drop
first when the line doesn't fit.
Expanded cards keep their paid/requested line, since it already shows
the totals, and show only the viewer part ("You owe $5", "✓ You paid
$5", "Your request") at the footer's trailing end, as in the design.
Replace the six-row list and the separate Custom wheel with one sheet: a readout, preset chips from 1 Day to 4 Weeks, a five-row grid of today through today + 28 days, and a Time row for a custom day that defaults to 6:00 PM, clamped to now + 1 h through now + 28 d - 5 min. Spec tests 34-39.
The reaction alert added on main switches over every message widget, and this branch adds the payment request widget. Use the snippet the reply quote already shows for it.
bmc08gt
force-pushed
the
feat/group-requests
branch
from
October 26, 2026 16:36
d221512 to
6ecb2ff
Compare
…oses
Replace the Time To Pay readout with a Starts field ("Today, on send")
and an outlined Closes field over a summary line. The grid draws today
as a grey circle and the closing day as a white one, joined by a band
that wraps rounded at the row edges; a deadline later today shows only
the white circle. Spec tests 34, 35 and 38 updated, 40 added.
…ection The sheet kept the system glass background, which flashed across the whole sheet on every tap; give it the app background like the other sheets. The white day circle and chip highlight now slide to the new pick and the range band grows or shrinks with it. The Time row always shows: for a preset it reads the time the preset closes if sent now, and changing it makes that day custom.
The closing-day circle and the chip highlight were drawn per item and matched between them, so SwiftUI cross-faded them and they turned grey mid-move. Each is now one shape drawn under the items at the selected item's bounds, so it slides solid white. Done uses the filled button style the other request screens use.
Each day drew its own slice of the band and animated it on its own, so a change that crossed weeks showed a vertical seam where a growing slice met a finished one. Each week row now draws a single shape that grows from the start side.
… under Reduce Motion Spec 3bc80ce test 44. Also covers tests 41, 42 and 45, and the Time row in tests 34 and 38.
…in the circle Spec 243abdb tests 46 and 47. The label sat pinned to the top of the 38 pt cell, on the circle's edge; it now stacks above the number as one centred group, and its grey follows the cell's state.
…ile it slides The band animated its corner radius, so an end moving with the circle across a week rounded for a few frames. It is now a square-ended rectangle masked by a capsule spanning the row, so only the row's edges round. Adds spec test 48.
* commit '28622a831ce5876c6165063aed9d4203292651df': feat(history): move the boost emoji and chat into the detail subtitle feat(history): show the boost on the detail header and receipt card feat(history): title boosted payments and link them to the boosted message fix(chat): land message jumps centred on the target (#1136) # Conflicts: # FlipcashCore/Sources/FlipcashStore/Database.swift
bmc08gt
marked this pull request as ready for review
October 26, 2026 19:24
CI's Xcode 26.3 timed out type-checking RootSheetHost's body once it gained the full-screen cover, and found 1 - 2 * stripCollapse ambiguous. The two root-sheet bindings now come from one helper, and the opacity converts the CGFloat to Double explicitly.
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.
Group payment requests in private groups, built from the shared spec (code-payments/flipcash-client-orchestrator#63). Based on
feat/group-send(#1128) for its Select People screen.The request title and description are sealed and opened through shared-core's
encryptPaymentRequestText/decryptPaymentRequestText(code-payments/code-android-app#1852), pinned at shared-core 0.16.0.What it adds
SelectPeopleList, and sends thepaymentRequestwidget with one idempotency key per confirm. The give-cash gate applies only to Send, since requesting needs no balance.ChatPaymentRequestCelldraws the card from thePaymentRequestsstore. A widget whose request belongs to another chat, or whose creator isn't the sender, draws the unsupported bubble. Quotes and the chat-list preview read "requested".Sessionpays under the server'spayment_intent_idthrough a newRequestedPayingpath withChatPaymentMetadata.requestedPayment. The intent ID stays in one private field of the pay model and is never logged or reported.DirectSending.sendkeeps its signature.Database.schemaVersionis bumped for the new widget content.Where it differs from the spec
ChatSeal. Decryption is checked byte for byte, encryption only by round trip, because the nonce'd encrypt overload is a Kotlin default argument and isn't exported to Swift. The exact-encrypt check runs in Android'sPaymentRequestTextVectorTest..sheetoff the Money sheet, not anAppRoutercase.Tests
PaymentRequestSpecTestscovers spec cases 1–20 plus the details rows, deadline presets and title rules. Case 21 isPaymentRequestTextVectorTestsinFlipcashCoreVectors, withpayment_request_text.jsoncopied from the orchestrator'stest-vectors/.