From c2c97b774c8fe1485cb032e2a36085392a60e8b4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 26 Oct 2026 18:17:18 -0400 Subject: [PATCH] fix(group-requests): keep the strip's text and Pay button whole as the deck collapses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The moving card that carries the front request back into the owed strip drew no Pay button and laid the strip's title and owed line out at exactly their measured widths. On the frame before the real strip took over, the strip showed at full width with no Pay button and both lines truncated ("Monday Requ…", "You owe $2 · 7 days…"). The moving card now draws the Pay button at its measured spot, held against its trailing edge and fading in over the last 35% of the collapse, and gives the strip's text a point of slack over its measured width so it doesn't round down into truncation. --- .../PaymentRequest/PaymentRequestDeck.swift | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/Flipcash/Core/Screens/PaymentRequest/PaymentRequestDeck.swift b/Flipcash/Core/Screens/PaymentRequest/PaymentRequestDeck.swift index 475c899c9..a4731e002 100644 --- a/Flipcash/Core/Screens/PaymentRequest/PaymentRequestDeck.swift +++ b/Flipcash/Core/Screens/PaymentRequest/PaymentRequestDeck.swift @@ -528,6 +528,8 @@ enum DeckPart: Hashable { case ring case title case amounts + /// The strip's Pay button, which the moving card fades out on its way into the list and back in as it lands. + case pay /// The parts only the expanded card shows. case label case bar @@ -630,14 +632,10 @@ struct PaymentRequestPillFace: View { .truncationMode(.tail) Spacer(minLength: 0) if item.owedShare != nil { - Button("Pay") { pay?(item.id) } - .opacity(max(0, 1 - 2 * Double(stripCollapse))) - .font(.appTextSmall) - .foregroundStyle(Color.backgroundMain) - .padding(.horizontal, 18) - .frame(height: 32) - .background(Color.success, in: .capsule) + Button { pay?(item.id) } label: { PaymentRequestStripPayLabel() } .buttonStyle(.plain) + .deckPart(.pay, measure) + .opacity(max(0, 1 - 2 * Double(stripCollapse))) } } .padding(.horizontal, 12) @@ -655,6 +653,18 @@ struct PaymentRequestPillFace: View { } } +/// The strip's green Pay capsule. +private struct PaymentRequestStripPayLabel: View { + var body: some View { + Text("Pay") + .font(.appTextSmall) + .foregroundStyle(Color.backgroundMain) + .padding(.horizontal, 18) + .frame(height: 32) + .background(Color.success, in: .capsule) + } +} + /// A request in the expanded deck: the timeline card's lines under the ring and title, on the app's /// opaque card surface. private struct PaymentRequestDeckCard: View { @@ -861,6 +871,7 @@ private struct PaymentRequestDeckMorph: View, Animatable { Group { ring(p) + stripPay(in: box, from: pillBox, opacity: amountsOut) shared(.title, scale: titleScale, p: p, outgoing: outgoing, incoming: incoming) { isCard in Text(item.card.title) .font(isCard ? .appTextMedium : source.titleFont) @@ -932,9 +943,11 @@ private struct PaymentRequestDeckMorph: View, Animatable { if let from = pill[part], let to = card[part] { let corner = CGPoint(x: from.minX + (to.minX - from.minX) * p - origin.x, y: from.minY + (to.minY - from.minY) * p - origin.y) let grow = 1 + (scale - 1) * p + // A point over the measured width: laid out at exactly its own fractional width, the face's + // text can round down and truncate. text(false) .dynamicTypeSize(pillTypeSize) - .frame(width: from.width, height: from.height, alignment: .topLeading) + .frame(width: from.width + 1, height: from.height, alignment: .topLeading) .scaleEffect(grow, anchor: .topLeading) .offset(x: corner.x, y: corner.y) .opacity(outgoing) @@ -946,6 +959,19 @@ private struct PaymentRequestDeckMorph: View, Animatable { } } + /// The strip's Pay button, held the same distance from the moving container's trailing edge so it + /// is in place before the card lands, rather than appearing a frame after it. + @ViewBuilder + private func stripPay(in box: CGRect, from pillBox: CGRect, opacity: CGFloat) -> some View { + if source == .strip, item.owedShare != nil, let rest = pill[.pay] { + PaymentRequestStripPayLabel() + .dynamicTypeSize(pillTypeSize) + .frame(width: rest.width, height: rest.height) + .offset(x: box.maxX - (pillBox.maxX - rest.minX) - origin.x, y: box.minY + (rest.minY - pillBox.minY) - origin.y) + .opacity(opacity) + } + } + /// A part only the expanded card shows, kept at the same relative spot inside the moving container. @ViewBuilder private func extra(