Skip to content

feat(activity): open a transaction's details from any activity row - #1417

Merged
bmc08gt merged 1 commit into
code/cashfrom
feat/transaction-details
Sep 5, 2026
Merged

feat(activity): open a transaction's details from any activity row#1417
bmc08gt merged 1 commit into
code/cashfrom
feat/transaction-details

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

An activity row states what moved and when, and nothing else. Tapping one did nothing, so the exchange rate, the token quantity, the fee, and the settlement state had nowhere to be read, and a sent cash link could only be cancelled from the row's own swipe action.

Rows in the wallet preview, the token-info preview and the full activity history now push AppRoute.Sheets.TransactionDetails(messageId), which draws the same entry as a screen: heading and avatar, the signed amount, and a receipt of the values a row has no space for. Figma node 9708:105260.

How it resolves

TransactionDetailsMapper shares TransactionItemMapper's reading of the metadata — counterparty, avatar, direction — so a row and the screen it opens cannot disagree about what the entry was. What it adds is the kind stated in the user's own voice, the receipt values, and the two actions.

ResolvedTransaction carries the drawn state and the action targets in one emission. Cancelling needs IndirectlySentCrypto.creator; opening the conversation needs the user id plus the profile. Neither belongs in a UI model, and opening three flows on one id invites them to disagree.

The screen reads through MessageDao.observeMessageById, so cancelling a cash link from the app bar redraws the screen once the update lands rather than leaving a stale "Pending".

AvatarSlot moves out of ActivityFeedRow into TransactionAvatarImage, parameterised by size, so the screen opens on the same avatar that was tapped rather than a second rendering of it. Its defaults are the row's geometry unchanged.

Where the ViewModel lives

TransactionDetailsViewModel sits in :features:transactions, not beside the mapper. Cancelling goes through TokenCoordinator, and :shared:transaction-history depending on :shared:tokens closes a cycle through chat.

Two receipt rows are unreachable from real data

Neither the message metadata nor the notification carries a withdrawal destination or a deposit source — IndirectlySentCrypto.creator is a gift-card vault, not an account — so account is always null and the To/From row never renders. A legacy buy/sell records only the mint that moved, so its subtitle has no counterpart mint to name, and PaidCrypto carries a pool id with no name. Both are built and left in place for when the server sends them.

The token quantity is an estimate. estimatedTokenAmountIn prices against the mint's current supply, so on a historical entry it says what that value is worth now, not what it bought then.

An activity row states what moved and when, and nothing else. Tapping one did
nothing, so the exchange rate, the token quantity, the fee, and the settlement
state had nowhere to be read, and a sent cash link could only be cancelled from
the row's own swipe action.

Rows in the wallet preview, the token-info preview and the full activity history
now push `AppRoute.Sheets.TransactionDetails(messageId)`, which draws the same
entry as a screen: heading and avatar, the signed amount, and a receipt of the
values a row has no space for.

`TransactionDetailsMapper` shares `TransactionItemMapper`'s reading of the
metadata (counterparty, avatar, direction), so a row and the screen it opens
cannot disagree about what the entry was. What it adds is the kind stated in the
user's own voice, the receipt values, and the two actions. `ResolvedTransaction`
carries the drawn state and the action targets in one emission: cancelling needs
`IndirectlySentCrypto.creator`, and opening the conversation needs the user id
plus the profile, neither of which belongs in a UI model.

The screen reads through `MessageDao.observeMessageById`, so cancelling a cash
link from the app bar redraws the screen once the update lands rather than
leaving a stale "Pending".

`TransactionDetailsViewModel` lives in `:features:transactions` rather than
beside the mapper: cancelling goes through `TokenCoordinator`, and
`:shared:transaction-history` depending on `:shared:tokens` closes a cycle
through chat.

Two receipt rows are built but unreachable from real data. Neither the message
metadata nor the notification carries a withdrawal destination or a deposit
source, so `account` is always null and the To/From row never renders; and a
legacy buy/sell records only the mint that moved, so its subtitle has no
counterpart mint to name. Both are left in place for when the server sends them.

The token quantity is an estimate: `estimatedTokenAmountIn` prices against the
mint's current supply, so on a historical entry it says what that value is worth
now, not what it bought then.
@bmc08gt bmc08gt self-assigned this Sep 4, 2026
@github-actions github-actions Bot added type: feature New functionality area: payments Payments, transfers, intents, billing area: tokens Token accounts, balances, token info and removed type: feature New functionality labels Sep 4, 2026
@bmc08gt
bmc08gt merged commit 06cf977 into code/cash Sep 5, 2026
3 checks passed
bmc08gt added a commit to code-payments/code-ios-app that referenced this pull request Sep 8, 2026
* feat(activity): open a tapped activity row's details

An activity row was a label everywhere it was drawn, so the only way to
read an entry's rate, token quantity or id was not to have one, and the
only way to cancel a pending cash link was to find it again in the
per-token history. Android closed this in
code-payments/code-android-app#1417 and
code-payments/code-android-app#1418; this is the same screen on iOS.

`TransactionDetails` maps an `Activity` to what the screen shows: which
of the 13 kinds it was, its heading and subtitle, whether it can be
cancelled, and whether it has a conversation to open. The three cash
kinds stay distinct because `MessageMetadata` already distinguishes
them, and there is no received-cash-link case because a claimed link
arrives as a plain receive. `TransactionDetailsTests` mirrors Android's
`TransactionDetailsMapperTest` case for case.

`ActivityRow` now pushes `transactionDetails` itself rather than each of
its three call sites wiring a tap, so a row opens the same screen in the
wallet's Recent section, the currency info screen, the cross-token
history and a token's own history. The avatar the row draws moves to
`ActivityAvatar`, which the details header draws at 80pt from the same
`ActivityResolution`, so the screen opens on exactly the avatar that was
tapped. `TransactionHistoryScreen` drops its own cancel dialog; cancel
now lives in the details screen's toolbar, as it does on Android.

Layout follows Figma node 9708:105260, and the kind headings node
9708:118186, including the four places Android's implementation is still
waiting on design: amount type size, line-item value size, gutter inset,
and the header avatar's ring.

* fix(activity): push the chat from transaction details instead of switching tabs

"View in Chat" called `navigate(to:)`, which reaches the Chat tab's stack by
bringing the tab forward. That swaps the tab out from under the transition: the
tab bar and the conversation list are both visible for a beat before the chat
pushes in over them.

Push it onto the stack the details screen is already on. The chat slides in from
the entry it belongs to, back returns to the entry, and nothing changes
underneath. `ConversationScreen` names no stack of its own, and the pushes it
makes — a profile, a currency — resolve against whichever stack is topmost, so
it carries over unchanged.
bmc08gt added a commit that referenced this pull request Sep 8, 2026
* fix(activity): read the transaction's recorded token quantity

#1417 shipped the details screen's Tokens row as an estimate, and said so: it
priced the entry's value against the mint's current circulating supply, so a
historical entry stated what that value buys today rather than what moved at the
time. The feed has carried the real number all along.

Every entry's amount comes from the proto's `CryptoPaymentAmount`, and
`ActivityFeedMessageMapper` puts `quarks` straight into
`LocalFiat.underlyingTokenAmount` — USD quarks for the reserve, that mint's own
quarks for anything else. It survives persistence intact as
`MessageEntity.amountUsdc`. `tokenAmountOf` now scales that by the mint's
decimals and formats it, with no curve in the path.

Dropping the round trip also drops two scale bugs it carried. It sold token
quarks to a USD value at `token.decimals` and bought them back at a hardcoded
six, which is wrong for every launchpad mint at ten; and it priced against
`launchpadMetadata?.currentCirculatingSupplyQuarks ?: 0`, so a mint whose
metadata had not resolved was valued against a supply of zero.

Trailing zeros are now trimmed rather than padded to the mint's decimals, since
`1,204.9050000000` states no more than `1,204.905`. A USDF entry that read
`20.000000` reads `20`. iOS reads the same field for the same row in
code-payments/code-ios-app#733, so the two screens state one number.

The screenshot fixtures declared a six-decimal Jeffy, which no launchpad mint
is; it is ten now, and the hand-written USDF quantities match what the mapper
actually produces.

* refactor(activity): move the token quantity formatting onto Token

The details mapper was holding the BigDecimal shift and the DecimalFormat
itself, which is scale arithmetic rather than mapping. `Token.formattedQuantity`
takes both, next to the `decimals` the shift reads.

FeedItemDetails needs the same reading, so having one place for it is the
point.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: payments Payments, transfers, intents, billing area: tokens Token accounts, balances, token info

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant