Skip to content

fix: Transaction Details offline — DB-first + honest offline state [PERA-4580]#1008

Merged
fmsouza merged 12 commits into
mainfrom
fmsouza/pera-4580
Jul 24, 2026
Merged

fix: Transaction Details offline — DB-first + honest offline state [PERA-4580]#1008
fmsouza merged 12 commits into
mainfrom
fmsouza/pera-4580

Conversation

@fmsouza

@fmsouza fmsouza commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Description

Transaction Details now works offline instead of showing a misleading generic error or spinning forever:

  • DB-first rendering: the history lists (account history, asset transaction list) thread the tapped SQLite row through navigation (historyTransaction param); a new mapHistoryItemToDisplayableTransaction in @perawallet/wallet-core-transactions converts it to the displayable shape, so type, amounts, addresses, date, and fee render from local data with no network. The indexer fetch stays enabled online as enrichment and is preferred once it lands (online behavior unchanged).
  • Honest empty states: the screen's logic moved into a colocated useTransactionDetailsScreen hook with regime-aware states — offline (isPausederrors.network.no_connection copy + Retry) vs fetch error (typed PERA-4574 copy via getNetworkErrorMessageKeys + Retry) — replacing the unconditional errors.general EmptyView. Paused fetches auto-resume on reconnect.
  • Bounded placeholders: AssetTransferDisplay only shows a spinner while the asset query is genuinely pending; a settled miss shows "Asset unavailable" and self-hides the amount row rather than rendering an amount scaled with assumed decimals. AppCallTransactionDisplay guards the on-completion row (mapped rows don't carry it).
  • Offline inner-transactions hint: mapped app-call rows thread the stored innerTransactionCount through; when the full inner transactions aren't available locally, the screen shows "N Inner Transactions — Connect to the internet to view" instead of silently hiding the panel.

Design notes:

  • The mapper returns null for acfg/afrz/keyreg/hb rows (and axfer rows missing their asset summary) — the SQLite row is a lossy summary and can't honestly reconstruct those sub-structs, so offline those fall through to the offline state instead of fabricating details.
  • historyTransaction was added to both SigningStackParamList and RootStackParamList (the history hooks navigate through the root stack); both carry the existing non-serializability caveat.
  • Fields the row doesn't store (note, app args) self-hide via the sub-displays' existing guards, per the ticket's "never block on indexer-only fields".

Related Issues

Checklist

  • Have you tested your changes locally?
  • Have you reviewed the code for any potential issues?
  • Have you documented any necessary changes in the project's documentation?
  • Have you added any necessary tests for your changes?
  • Have you updated any relevant dependencies?

Additional Notes

  • TDD throughout: mapper spec (12 tests), screen-hook spec (9 tests, covers the ticket's three AC scenarios: offline-with-local-data renders, offline-without-data → offline state, error → typed error state), asset-display hook spec (+2 tests). Full suite green (547 files / 3922 tests), pnpm pre-push and pnpm build clean.
  • Also fixed in passing: bare Decimal(...) call (missing new) in PaymentTransactionDisplay.tsx.
  • The usePaymentTransactionDisplay unary-minus coercion is now fixed too: .negated() keeps the amount a Decimal end-to-end (new hook spec, 4 tests), and the downstream new Decimal(amount) re-wrap is gone.
  • CI Audit fix: this branch originally pinned fast-uri@<4: 3.1.4; main's fix(ios): align Expo modules to the 57.0.7 set to resolve dyld crash #1009 landed its own fix (fast-uri@<4.1.1: ">=4.1.1") in the meantime, so the merge adopted main's version — this PR's remaining deps delta vs main is only svgo@<3.3.4: 3.3.4 (GHSA-2p49-hgcm-8545, dev-only path via the svg transformer). Both audit gates (--prod --audit-level=moderate and informational --audit-level=low) are clean on the merged result.

🤖 Generated with Claude Code

@fmsouza
fmsouza requested a review from a team as a code owner July 22, 2026 14:31

@yasin-ce yasin-ce left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ Nits

  • firstValid: 0n / lastValid: 0n in mapHistoryItemToDisplayableTransaction.ts:1253-1254 are placeholders (the row doesn't store them). Fine if the detail display never surfaces validity rounds — worth a one-word confirm that nothing renders them as a literal 0.
  • The PaymentTransactionDisplay fixes (-algosalgos.negated(), dropping the bare Decimal(amount) wrapper) are genuine correctness improvements — -algos on a Decimal coerces through valueOf, and bare Decimal(...) violates the new Decimal convention. Slightly tangential to "offline," but they're in a directly-touched display and clearly right, so I'd keep them; just calling it out so it's a conscious inclusion.

🔵 Questions / awareness

  • isError is checked before isPaused in useTransactionDetailsScreen.ts:635-643 — the opposite ordering to sibling #1013, which deliberately prefers offline (isError && !hasInternet → offline). This one matches the documented §2 contract, and for a genuine offline error the typed copy is already errors.network.no_connection ("You appear to be offline"), so it's honest either way. The only gap: a device that goes offline carrying a stale non-offline error (e.g. a prior timeout) shows the timeout screen with a Retry that just re-pauses. Intentional divergence from #1013, or should both surfaces agree?
  • The offline retry button calls refetch() unconditionally (useTransactionDetailsScreen.ts:667-669) — no hasInternet short-circuit like #1013's charts have. Harmless (react-query just re-pauses), but the two offline PRs handle the same retry two different ways; worth picking one pattern.
  • AssetTransferDisplay now hides the amount row entirely on a settled asset miss ((asset || isAssetPending)) rather than rendering with assumed decimals — good, honest. Confirm the opt-in path (transferType === 'opt-in', always amount-less) still reads right with the new guard.

fmsouza added 2 commits July 24, 2026 12:50
…ne retry [PERA-4580]

Align the Transaction Details screen with the PERA-4581 charts contract
(PR #1013): a device that goes offline carrying a prior non-offline error
(e.g. a timeout) now shows the offline surface via
`isPaused || (isError && !hasInternet)` instead of a timeout screen with a
Retry that just re-pauses. The retry also short-circuits while offline so it
no longer fires a doomed request, matching the charts retry.

Addresses review feedback on #1008.
# Conflicts:
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
@fmsouza
fmsouza merged commit fe4c8fd into main Jul 24, 2026
9 checks passed
@fmsouza
fmsouza deleted the fmsouza/pera-4580 branch July 24, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants