feat(activity): open a transaction's details from any activity row - #1417
Merged
Conversation
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.
This was referenced Sep 8, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
TransactionDetailsMappersharesTransactionItemMapper'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.ResolvedTransactioncarries the drawn state and the action targets in one emission. Cancelling needsIndirectlySentCrypto.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".AvatarSlotmoves out ofActivityFeedRowintoTransactionAvatarImage, 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
TransactionDetailsViewModelsits in:features:transactions, not beside the mapper. Cancelling goes throughTokenCoordinator, and:shared:transaction-historydepending on:shared:tokenscloses 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.creatoris a gift-card vault, not an account — soaccountis 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, andPaidCryptocarries 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.
estimatedTokenAmountInprices against the mint's current supply, so on a historical entry it says what that value is worth now, not what it bought then.