Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,10 @@ private struct PaymentRequestPaySheetPresenter: ViewModifier {
pendingDeposit = nil
router.navigate(to: .currencyInfoForDeposit(mint))
}) {
// The share reads paid once the payment lands; the sheet stays up through its success state.
if let payment = flow.payment, let request = payment.request, let creator = request.creator {
// The share reads paid once the payment lands, so the sheet renders from what it opened
// with and stays up through its success state.
if let payment = flow.payment, let snapshot = payment.sheetSnapshot, let creator = snapshot.request.creator {
let request = snapshot.request
PaymentRequestPaySheet(
payment: payment,
payee: request.chatID.flatMap { chatID in
Expand All @@ -447,7 +449,7 @@ private struct PaymentRequestPaySheetPresenter: ViewModifier {
.first { $0.userID == creator }
},
payeeID: creator,
amount: payment.share.map { PaymentRequestCopy(session.displayCurrency).shareText($0, of: request) } ?? ""
amount: PaymentRequestCopy(session.displayCurrency).shareText(snapshot.share, of: request)
)
.task {
// Opened from the chat's strip, nothing else has loaded the roster that names the payee.
Expand Down
14 changes: 13 additions & 1 deletion Flipcash/Core/Screens/PaymentRequest/PaymentRequestPayment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,18 @@ final class PaymentRequestPayment {
case noBalance
}

/// The request and the viewer's unpaid share as they read when the swipe sheet opened.
struct SheetSnapshot: Equatable {
let request: GroupPaymentRequest
let share: GroupPaymentRequest.Share
}

let requestID: PaymentRequestID

/// What the swipe sheet shows, taken when ``prepare()`` finds the share payable; it outlives
/// the share reading paid, so the sheet keeps its amount through success and dismiss.
private(set) var sheetSnapshot: SheetSnapshot?

private(set) var selectedBalance: ExchangedBalance?
/// Set when the insufficient-funds dialog's Add Money is tapped; the presenter routes to the deposit and clears it.
var depositMint: PublicKey?
Expand Down Expand Up @@ -104,7 +114,9 @@ final class PaymentRequestPayment {
showRequestClosed()
return .closed
}
return intentID != nil && share != nil ? .ready : .nothingToPay
guard intentID != nil, let request, let share else { return .nothingToPay }
sheetSnapshot = SheetSnapshot(request: request, share: share)
return .ready
}

/// The share in the request's currency, nil when the request's currency or amount can't be read.
Expand Down
22 changes: 19 additions & 3 deletions Flipcash/Core/Screens/Send/GroupSendFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,24 @@ private struct GroupSendConfirmSheet: View {
}
}

/// Half of ``Font/appDisplayLarge``'s size, as "each" sits on the chat's cash card.
private static let eachFont: Font = .default(size: UIFont.appDisplayLarge.pointSize / 2, weight: .medium)

/// The amount, followed by a smaller "each" for a group. One `Text`, so the runs share a
/// baseline and shrink together, and "each" never wraps.
private var amountLine: Text {
var line = AttributedString(viewModel.eachAmountLabel ?? "")
line.swiftUI.font = Font.appDisplayLarge
line.swiftUI.foregroundColor = Color.textMain
if viewModel.isSplitAcrossSelection {
var each = AttributedString(" each")
each.swiftUI.font = Self.eachFont
each.swiftUI.foregroundColor = Color.textSecondary
line.append(each)
}
return Text(line)
}

var body: some View {
PartialSheet(background: .clear) {
VStack(spacing: 25) {
Expand All @@ -261,9 +279,7 @@ private struct GroupSendConfirmSheet: View {
.lineLimit(1)
}
VStack(spacing: 8) {
Text(viewModel.eachLabel ?? "")
.font(.appDisplayLarge)
.foregroundStyle(Color.textMain)
amountLine
.lineLimit(1)
.minimumScaleFactor(0.4)
if let total = viewModel.totalLabel {
Expand Down
11 changes: 8 additions & 3 deletions Flipcash/Core/Screens/Send/GroupSendViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,15 @@ final class GroupSendViewModel {
return FiatAmount(value: each.value * Decimal(selection.count), currency: each.currency)
}

/// The confirm sheet's large line: "$1.00 each" for two or more people, just the amount for one.
var eachLabel: String? {
/// The confirm sheet's large amount, "$1.00", without the "each" that follows it for a group.
var eachAmountLabel: String? {
guard let each = enteredAmountEach, !selection.isEmpty else { return nil }
return selection.count > 1 ? "\(each.formatted()) each" : each.formatted()
return each.formatted()
}

/// Whether the confirm sheet's amount is one person's part, followed by "each": two or more people.
var isSplitAcrossSelection: Bool {
selection.count > 1
}

/// The confirm sheet's smaller line, "$3.00 total"; nil for one person, who gets only the amount.
Expand Down
8 changes: 5 additions & 3 deletions FlipcashTests/GroupSendViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,20 @@ struct GroupSendViewModelTests {
#expect(viewModel.sendTitle == "Send \(try #require(viewModel.enteredTotal).formatted())")
}

@Test("The confirm sheet reads \"$1.00 each\" over \"$3.00 total\", or just the amount for one person")
@Test("The confirm sheet reads \"$1.00\" with \"each\" over \"$3.00 total\", or just the amount for one person")
func confirmLabels() async throws {
let (viewModel, _) = try await make(total: "1")
let a = UUID()
viewModel.toggle(a)

#expect(viewModel.eachLabel == "$1.00")
#expect(viewModel.eachAmountLabel == "$1.00")
#expect(viewModel.isSplitAcrossSelection == false)
#expect(viewModel.totalLabel == nil)

[UUID(), UUID()].forEach(viewModel.toggle)

#expect(viewModel.eachLabel == "$1.00 each")
#expect(viewModel.eachAmountLabel == "$1.00")
#expect(viewModel.isSplitAcrossSelection)
#expect(viewModel.totalLabel == "$3.00 total")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,61 @@ struct PaymentRequestPaymentNoBalanceTests {
#expect(container.session.dialogItem?.title == "You Need More Cash")
}
}

@MainActor
@Suite("PaymentRequestPayment sheet snapshot")
struct PaymentRequestPaymentSheetSnapshotTests {

private let requestID = PaymentRequestID(data: Data([2]))

/// What the server returns on the next refetch.
@MainActor
private final class Server {
var isPaid = false
}

private func request(for userID: UserID, paid: Bool) -> GroupPaymentRequest {
GroupPaymentRequest(
id: requestID,
creator: UUID(),
createdAt: .now.addingTimeInterval(-60),
status: .open,
closedAt: nil,
version: paid ? 2 : 1,
chatID: ConversationID(data: Data(repeating: 0x0C, count: 32)),
split: .init(
text: .plaintext(title: "Dinner", description: ""),
currency: "usd",
endsAt: .now.addingTimeInterval(86_400),
shares: [.init(
userID: userID,
nativeAmount: 10,
paidAt: paid ? .now : nil,
settlement: paid ? .intent(Data(repeating: 9, count: 32)) : nil
)]
)
)
}

@Test("The sheet keeps the share it opened with after a refetch reads it paid")
func sheetSnapshot_survivesShareReadingPaid() async throws {
let container = try SessionContainer.makeTest(holdings: [])
let userID = container.session.userID
let server = Server()
let requests = PaymentRequests(getChatPage: { _, _, _ in [] }, getRequest: { _ in
(request(for: userID, paid: server.isPaid), Data(repeating: 7, count: 32))
})
let payment = PaymentRequestPayment(requestID: requestID, sessionContainer: container, requests: requests)

#expect(await payment.prepare() == .ready)
let opened = try #require(payment.sheetSnapshot)

server.isPaid = true
_ = try await requests.refresh(requestID)

#expect(payment.share == nil)
#expect(payment.sheetSnapshot == opened)
#expect(opened.share.nativeAmount == 10)
#expect(opened.share.isPaid == false)
}
}
Loading