Skip to content

feat(group-requests): ask a private group for money and pay a share - #1129

Merged
bmc08gt merged 47 commits into
mainfrom
feat/group-requests
Oct 26, 2026
Merged

bmc08gt merged 47 commits into
mainfrom
feat/group-requests

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Create: in a private group the money button opens a Send / Request sheet. Request walks through amount, summary (name, people, deadline) and Select People, now a shared SelectPeopleList, and sends the paymentRequest widget with one idempotency key per confirm. The give-cash gate applies only to Send, since requesting needs no balance.
  • Transcript: a SwiftUI-hosted ChatPaymentRequestCell draws the card from the PaymentRequests store. 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".
  • App-bar deck: a pill in the trailing toolbar item. Swipe down cycles it, and a tap expands it into the card list by matching the front card from the pill's measured frame. VoiceOver gets a "Next request" action. Requests that ended in the last 24 h stay in the deck, greyed.
  • Details: amounts, status line, per-person rows, and Send for the viewer's own share. The creator gets Close Request under "•••" and Mark as Paid on unpaid rows, each behind a confirm. Editing stays hidden because the server doesn't support it.
  • Pay: Session pays under the server's payment_intent_id through a new RequestedPaying path with ChatPaymentMetadata.requestedPayment. The intent ID stays in one private field of the pay model and is never logged or reported. DirectSending.send keeps its signature.
  • Refetch: the chat polls every 45 s while it's visible and active, refetches when a paid-share cash message arrives, and merges by version as the spec describes.
  • History: Request Money history pages through the resolved section 20 at a time.

Database.schemaVersion is bumped for the new widget content.

Where it differs from the spec

  • The vectors run through 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's PaymentRequestTextVectorTest.
  • One reject case varies an AAD label that the Swift API can't express. It runs only on the Kotlin side.
  • The request flow is a local .sheet off the Money sheet, not an AppRouter case.

Tests

PaymentRequestSpecTests covers spec cases 1–20 plus the details rows, deadline presets and title rules. Case 21 is PaymentRequestTextVectorTests in FlipcashCoreVectors, with payment_request_text.json copied from the orchestrator's test-vectors/.

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
bmc08gt force-pushed the feat/group-requests branch from d221512 to 6ecb2ff Compare October 26, 2026 16:36
…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
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.
@bmc08gt
bmc08gt merged commit 51b780e into main Oct 26, 2026
4 checks passed
@bmc08gt
bmc08gt deleted the feat/group-requests branch October 26, 2026 19:47
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