From bb7676c623bec4d0a65fe62d1c156d2158826735 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 8 Sep 2026 19:50:09 -0400 Subject: [PATCH 1/2] fix(transactions): tighten the gap between the details cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The receipt card, ID card and View in Chat button sat 16pt apart, one spacing for the whole screen. Figma node 9708:118142 puts 8pt between them and a much wider gap above, between the header and the block — they read as one stacked group, not as four evenly spaced items. Groups the three in their own 8pt stack, leaving the header on the outer stack's 16pt. The header gap stays as it was; Figma draws 43pt there and Android draws 10pt, so the two disagree and that one is worth settling before changing. Android spaces all four at `grid.x2`, which is 10dp at NORMAL width. This follows Figma's 8, as the button fill did. --- .../Main/Home/TransactionDetailsScreen.swift | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift b/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift index 1f20daeb5..29b8ea4b3 100644 --- a/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift +++ b/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift @@ -43,6 +43,10 @@ struct TransactionDetailsScreen: View { /// gives the header. private static let avatarSize: CGFloat = 80 + /// The gap between the cards, per Figma node 9708:118142 — tighter than the + /// gap that separates the header from them. + private static let cardSpacing: CGFloat = 8 + private var details: TransactionDetails { TransactionDetails( activity: activity, @@ -57,21 +61,27 @@ struct TransactionDetailsScreen: View { ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 16) { header - receiptCard - idCard - - if let userID = activity.counterparty?.userID, details.canViewInChat { - Button("View in Chat") { - // Pushed onto the stack this screen is already on, - // not routed to the Chat tab: a cross-stack jump - // swaps the tab out from under the transition, so - // the bar and the conversation list both show - // before the chat lands. Pushed, the chat arrives - // from the entry it belongs to and back returns - // here. - router.push(.tipConversationForUser(userID)) + + // The cards are one block, at the tighter gap Figma sets + // between them; the header sits apart from that block, so + // its gap is the outer stack's rather than this one's. + VStack(spacing: Self.cardSpacing) { + receiptCard + idCard + + if let userID = activity.counterparty?.userID, details.canViewInChat { + Button("View in Chat") { + // Pushed onto the stack this screen is already + // on, not routed to the Chat tab: a cross-stack + // jump swaps the tab out from under the + // transition, so the bar and the conversation + // list both show before the chat lands. Pushed, + // the chat arrives from the entry it belongs to + // and back returns here. + router.push(.tipConversationForUser(userID)) + } + .buttonStyle(.filled05) } - .buttonStyle(.filled05) } } .padding(.horizontal, 20) From 356bcbb45bdda5e7f886220f759bf91dab3fbd4e Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 8 Sep 2026 19:53:09 -0400 Subject: [PATCH 2/2] fix(transactions): drop the token badge from the details avatar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The details header reused the row's avatar wholesale, badge included, so the token appeared twice: once as a coin over the face, and again by name in the line under the amount. Figma draws the header avatar plain — the 80pt frame under node 9708:118182 has no badge child. The badge stays on the row, where the avatar is the only place the token reads. `ActivityAvatar` takes a `showsTokenBadge` flag instead of always deriving it from the activity, and the details header passes false. Turning it off also drops the 8pt trailing overhang the badge reserved, which had been pushing the centred header avatar 4pt off centre. --- .../Screens/Main/Home/ActivityAvatar.swift | 25 +++++++++++++------ .../Main/Home/TransactionDetailsScreen.swift | 10 +++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift b/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift index 595c8fa12..e84a85066 100644 --- a/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift +++ b/Flipcash/Core/Screens/Main/Home/ActivityAvatar.swift @@ -11,7 +11,7 @@ import FlipcashCore /// activity (tips/sends), the token image for token activity (deposits, buys), /// two overlapping coins for a conversion, or a monogram fallback. A peer avatar /// shows a face rather than a token, so it carries the transacted token as a coin -/// badge (Figma 8966:1910, 9717:14215). +/// badge (Figma 8966:1910, 9717:14215) unless the caller turns it off. /// /// Shared by the activity row and the transaction details header at different /// sizes, so the details screen opens on exactly the avatar that was tapped. Every @@ -22,6 +22,11 @@ struct ActivityAvatar: View { let resolution: ActivityResolution var size: CGFloat = 40 + /// Whether a peer avatar carries its token badge. The details header names the + /// token in full under the amount, so it turns the badge off rather than + /// saying the same thing twice at two sizes. + var showsTokenBadge: Bool = true + @Environment(SessionContainer.self) private var sessionContainer private var session: Session { sessionContainer.session } @@ -41,25 +46,31 @@ struct ActivityAvatar: View { .frame(width: size, height: size) .clipShape(Circle()) .overlay(alignment: .bottomTrailing) { - if Self.showsTokenBadge(for: activity) { + if drawsTokenBadge { tokenBadge.offset(x: badgeOverhang, y: badgeOverhang) } } // Reserves the badge's overhang so it doesn't eat into the gap // before whatever sits beside the avatar. - .padding(.trailing, Self.showsTokenBadge(for: activity) ? badgeOverhang : 0) + .padding(.trailing, drawsTokenBadge ? badgeOverhang : 0) } } - /// Whether the avatar carries a token badge: only a peer activity, whose - /// avatar is the counterparty rather than the token itself. + /// Whether this avatar draws a badge: one the activity calls for, that the + /// caller hasn't turned off. + private var drawsTokenBadge: Bool { + showsTokenBadge && Self.showsTokenBadge(for: activity) + } + + /// Whether the activity's avatar calls for a token badge: only a peer + /// activity, whose avatar is the counterparty rather than the token itself. static func showsTokenBadge(for activity: Activity) -> Bool { activity.swapMetadata == nil && activity.counterparty != nil } /// The token the payment moved in, as a coin badge over the counterparty's - /// avatar (Figma 9717:14140) — a peer activity shows *who*, so this badge is - /// the only place the token reads. + /// avatar (Figma 9717:14140) — a peer row shows *who*, so in a row this badge + /// is the only place the token reads. @ViewBuilder private var tokenBadge: some View { tokenCoin( url: resolution.imageURL(for: activity.exchangedFiat.mint, fallback: resolution.entryMint, session: session), diff --git a/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift b/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift index 29b8ea4b3..ffd45b6ef 100644 --- a/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift +++ b/Flipcash/Core/Screens/Main/Home/TransactionDetailsScreen.swift @@ -113,7 +113,15 @@ struct TransactionDetailsScreen: View { private var header: some View { VStack(spacing: 24) { VStack(spacing: 16) { - ActivityAvatar(activity: activity, resolution: resolution, size: Self.avatarSize) + // No token badge here, unlike the row: the line under the + // amount already names the token, and Figma draws the header + // avatar plain. + ActivityAvatar( + activity: activity, + resolution: resolution, + size: Self.avatarSize, + showsTokenBadge: false + ) Text(details.title) .font(.appTextLarge)