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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion CrossPlatformVectors/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ let sharedCore: Package.Dependency = {
}
return .package(path: "\(sharedCoreLocalRoot)/kmp/shared-core/spm")
}
return .package(url: "https://github.com/code-payments/flipcash-shared-core-spm", .upToNextMinor(from: "0.19.0"))
return .package(url: "https://github.com/code-payments/flipcash-shared-core-spm", .upToNextMinor(from: "0.20.0"))
}()

// Cross-platform test-vector GATE (Track-B/C safety net). Asserts the ed25519 this app signs
Expand Down
11 changes: 9 additions & 2 deletions Flipcash/Core/Screens/Conversation/ChatItem+Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ extension ChatItem {
/// How a payment-request widget draws; resolved by the caller from the request store so
/// the mapper stays pure. Defaults to the loading card.
paymentRequest: (ConversationMessage) -> PaymentRequestCardState = { _ in .loading },
/// The Send Back row under a cash message; nil draws none. See ``SendBackRow/resolve``.
sendBack: (ConversationMessage) -> ChatCashContent.SendBack? = { _ in nil },
/// Where the viewer's unread messages began at open; the divider heads the first message
/// after it that someone else sent.
unreadBoundary: UnreadBoundary = .none,
Expand Down Expand Up @@ -292,7 +294,7 @@ extension ChatItem {
case .cash(let fiat):
let branding = cashBranding(fiat)
let isTip = message.cashAction == .tipped
let card = cashCard(
var card = cashCard(
message.cashPayments,
receiver: message.cashReceiverID,
isFromSelf: isFromSelf,
Expand All @@ -305,12 +307,17 @@ extension ChatItem {
selfUserID: selfUserID,
nameOf: nameOf
)
// A return is only ever posted into a DM, so it is named for the DM's two people.
if message.cashReturn?.returnOf != nil, !namesAuthors {
card.lines = ChatCashContent.Lines.sentBack(isFromSelf: isFromSelf, counterpartName: counterpartName)
}
content = .cash(ChatCashContent(
amount: card.amount,
token: branding.token,
iconURL: branding.iconURL,
isTip: isTip,
lines: card.lines
lines: card.lines,
sendBack: sendBack(message)
))
case .deleted(let deletion):
content = .deleted(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable {
let onCashCardTap: (String) -> Void
/// Fired with a payment request's id bytes when its card is tapped. The owner opens Request details.
var onPaymentRequestTap: (Data) -> Void = { _ in }
/// Fired with a cash message's id when its receiver taps Send Back.
var onSendBack: (String) -> Void = { _ in }
/// Fired when the user taps a URL in a message. The owner routes it through the deep-link handler,
/// falling back to the system browser.
let onOpenURL: (URL) -> Void
Expand Down Expand Up @@ -183,6 +185,7 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable {
screen.onRetry = onRetry
screen.onCashCardTap = onCashCardTap
screen.onPaymentRequestTap = onPaymentRequestTap
screen.onSendBack = onSendBack
screen.onOpenURL = onOpenURL
screen.onMentionTap = onMentionTap
screen.onShareProfile = onShareProfile
Expand Down Expand Up @@ -248,6 +251,7 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable {
screen.onRetry = onRetry
screen.onCashCardTap = onCashCardTap
screen.onPaymentRequestTap = onPaymentRequestTap
screen.onSendBack = onSendBack
screen.onOpenURL = onOpenURL
screen.onMentionTap = onMentionTap
screen.onShareProfile = onShareProfile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ final class ConversationLoadCoordinator {
@ObservationIgnored private var expiryTask: Task<Void, Never>?
/// Where request widgets read their cards; nil draws them loading.
@ObservationIgnored private let paymentRequests: PaymentRequests?
/// Send Back for this open: the status check, the row state and the submit. Nil draws no row.
@ObservationIgnored let sendBack: SendBackController?
/// When this transcript opened; paid-share messages after it refetch their request once.
@ObservationIgnored private let openedAt: Date = .now
@ObservationIgnored private var refetchedPayments: Set<UInt64> = []
Expand All @@ -87,9 +89,11 @@ final class ConversationLoadCoordinator {
session: Session,
knownAuthors: KnownAuthorDirectory,
paymentRequests: PaymentRequests? = nil,
sendBack: SendBackController? = nil,
openingLoadLanded: Bool = false
) {
self.paymentRequests = paymentRequests
self.sendBack = sendBack
self.conversationID = conversationID
self.controller = controller
self.session = session
Expand Down Expand Up @@ -329,6 +333,19 @@ final class ConversationLoadCoordinator {
}
}
}
// Read inside the observation scope, so a status answer or a submit re-maps the row.
var sendBackRows: [UInt64: ChatCashContent.SendBack] = [:]
if let sendBack {
let returned = SendBackRow.returnedIDs(in: window)
for message in window {
guard case .cash = message.content else { continue }
if SendBackRow.needsStatus(message, selfUserID: controller.selfUserID, returnedIDs: returned) {
sendBack.checkStatusIfNeeded(message.id)
}
let row = SendBackRow.resolve(message, selfUserID: controller.selfUserID, returnedIDs: returned, local: sendBack.local(for: message.id))
if let row { sendBackRows[message.id.value] = row }
}
}
return Inputs(
messages: window,
selfUserID: controller.selfUserID,
Expand Down Expand Up @@ -358,6 +375,7 @@ final class ConversationLoadCoordinator {
headsHistory: headsHistory,
unreadBoundary: unreadBoundary(in: window),
paymentRequestCards: paymentRequestCards,
sendBackRows: sendBackRows,
// Read inside the observation scope, so a pin or unpin re-maps the menus.
pinnedIDs: controller.pins.pinnedIDs(in: conversationID)
)
Expand Down Expand Up @@ -426,6 +444,7 @@ final class ConversationLoadCoordinator {
// here touches the network, and an answer landing cannot re-diff this window.
linkCard: { links in classifier.firstCard(in: links) },
paymentRequest: { inputs.paymentRequestCards[$0.id.value] ?? .loading },
sendBack: { inputs.sendBackRows[$0.id.value] },
unreadBoundary: inputs.unreadBoundary,
headsHistory: inputs.headsHistory
)
Expand Down Expand Up @@ -543,6 +562,8 @@ final class ConversationLoadCoordinator {
var unreadBoundary: UnreadBoundary
/// Each request widget's card in the window, keyed by raw message id.
var paymentRequestCards: [UInt64: PaymentRequestCardState] = [:]
/// Each cash message's Send Back row, keyed by raw message id; absent draws none.
var sendBackRows: [UInt64: ChatCashContent.SendBack] = [:]
/// The chat's pinned messages, which decide Pin or Unpin on each row's menu.
var pinnedIDs: Set<MessageID> = []

Expand Down
48 changes: 48 additions & 0 deletions Flipcash/Core/Screens/Conversation/ConversationScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ struct ConversationScreen: View {
@State private var reactionPickerRequest: ReactionPickerRequest?
/// The paid reaction whose sheet is open, once the "$" picker has chosen its emoji.
@State private var paidReactionTarget: ReactionBoostTarget?
/// The Send Back offer whose confirm sheet is open.
@State private var sendBackOffer: SendBackOffer?
/// The group send whose card was tapped, shown in the details sheet.
@State private var groupSendDetails: GroupSendDetails?
/// The payment request whose details sheet is open.
Expand Down Expand Up @@ -486,6 +488,7 @@ struct ConversationScreen: View {
onPaymentRequestTap: { data in
afterKeyboardLowered { paymentRequestDetails = PaymentRequestSelection(id: PaymentRequestID(data: data)) }
},
onSendBack: { id in afterKeyboardLowered { tapSendBack(id) } },
onOpenURL: openLink,
onMentionTap: openMention,
onShareProfile: shareProfile,
Expand Down Expand Up @@ -738,6 +741,11 @@ struct ConversationScreen: View {
.sheet(item: $paidReactionTarget) { target in
PaidReactionSheet(target: target, selfReactions: selfReactions(on: target.messageID))
}
.sheet(item: $sendBackOffer) { offer in
SendBackSheet(offer: offer) { offer in
_ = await coordinator?.sendBack?.confirm(offer)
}
}
.sheet(item: $reactorsRequest, onDismiss: {
guard let userID = pendingReactorProfile else { return }
pendingReactorProfile = nil
Expand Down Expand Up @@ -1844,6 +1852,45 @@ struct ConversationScreen: View {
}
}

/// Send Back for one open of `id`. Not gated on chat type: the row follows the message's offer.
private func makeSendBack(_ id: ConversationID) -> SendBackController? {
let router = router
return SendBackController(
conversationID: id,
services: LiveSendBackServices(session: session, ratesController: ratesController),
analytics: LiveSendBackAnalytics(),
presenter: LiveSendBackPresenter(session: session, ratesController: ratesController) { mint in
router.push(.currencyInfoForDeposit(mint))
}
)
}

/// The receiver's tap on Send Back: the balance check, then the confirm sheet or the
/// insufficient-balance modal. The id is the row's stable id; resolve it to the cash message.
private func tapSendBack(_ messageID: String) {
guard let conversationID,
let sendBack = coordinator?.sendBack,
let message = (coordinator?.loader.messages ?? []).first(where: { $0.stableID == messageID }),
case .cash(let fiat) = message.content,
let senderID = message.senderID
else { return }
let balance = session.balance(for: fiat.mint)
let offer = SendBackOffer(
conversationID: conversationID,
messageID: message.id,
senderID: senderID,
original: fiat,
recipientName: title,
token: balance?.name ?? "Cash",
iconURL: balance?.imageURL
)
Task {
if let confirmed = await sendBack.tap(offer) {
sendBackOffer = confirmed
}
}
}

/// The gate panel's CTA: buys the mint the requirement names, or opens add-cash when there is no
/// one token to buy — a requirement spanning every holding, and a dollar-token one, which is
/// satisfied by adding cash rather than by swapping into the thing the cash already is. Both
Expand Down Expand Up @@ -2120,6 +2167,7 @@ struct ConversationScreen: View {
session: session,
knownAuthors: sessionContainer.knownAuthors,
paymentRequests: sessionContainer.paymentRequests,
sendBack: makeSendBack(id),
openingLoadLanded: loadedConversationID == id
)
}
Expand Down
Loading
Loading