Transaction detail screen

ProReact Native · Expo

One payment in full: merchant header, the signed amount, a status badge and a category chip that recategorizes, the details as grouped rows, split and receipt actions and Report a problem. The split sheet rolls each person's share as you change the head count.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add @goodui-native-pro/transaction-detail-screen

Then run npx expo install --fix so react-native-svg match your Expo SDK. Files land in components/goodui-native/transaction-detail-screen.

Pro screens install from the license-gated @goodui-native-pro registry. Setup: Pro access.

  • Wrap the app in GestureHandlerRootView and ThemeProvider (the sheets render in its overlay layer).

Usage

// app/transaction/[id].tsx
import { Stack, router, useLocalSearchParams } from "expo-router";
import * as Clipboard from "expo-clipboard";
import { TransactionDetailScreen } from "@/components/goodui-native/transaction-detail-screen/transaction-detail-screen";
import { api, useTransaction } from "@/lib/bank";

export default function TransactionRoute() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const { transaction, hasReceipt } = useTransaction(id);
  return (
    <>
      <Stack.Screen options={{ headerShown: false }} />
      <TransactionDetailScreen
        transaction={transaction} // { merchant, amount: -46.5, currency: "EUR", date, status, category, card, reference }
        onCategoryChange={(category) => api.recategorize(id, category)} // rejects: the chip goes back
        onSplit={(people, share) => api.requestSplit(id, people, share)}
        hasReceipt={hasReceipt}
        onReceiptPress={() => (hasReceipt ? router.push(`/receipt/${id}`) : api.attachReceipt(id))}
        onReport={(reason) => api.openDispute(id, reason.key)} // resolve with the case number
        onCopyReference={(ref) => Clipboard.setStringAsync(ref)}
        onBack={() => router.back()}
      />
    </>
  );
}

When to use

Use it for
  • Opening a payment from a bank, card, wallet or expense app's transaction list.
  • Expense tools where people fix the category, attach a receipt or split a bill after paying.
Reach for something else
  • A list of transactions: use wallet-home-screen or list-row rows.
  • A transfer being set up (amount entry, recipient): this screen shows a payment that already happened.
  • Order details from a shop: use order-tracking-screen.

Examples

On its own, with a sample payment

<TransactionDetailScreen onBack={() => router.back()} />

Paid abroad, still pending

<TransactionDetailScreen
  transaction={{
    id: "tx_19", merchant: "Blue Bottle Coffee", amount: -6.72, currency: "EUR", date: "2026-10-30T08:14:00Z",
    status: "pending", category: "Eating out", card: { name: "Travel", last4: "7731" }, reference: "US44 0021 77",
    original: { amount: 7.25, currency: "USD", rate: 0.9269 },
  }}
/>

API reference

PropTypeDefault
transactionTransactionDetaildefaultTransaction
categories
Categories offered by the category chip.
string[]defaultCategories
onCategoryChange
Called with the new category. Return a promise; the chip goes back if it rejects.
(category: string) => unknown—
onSplit
Split sheet: called with the number of people (you included) and each share. Return a promise; the sheet's button shows its states.
(people: number, share: number) => unknown—
hasReceipt
Whether a receipt is attached. Switches the button between Attach receipt and View receipt.
booleanfalse
onReceiptPress
Attach or open the receipt. Return a promise to show a spinner while a photo uploads.
() => unknown—
reportReasons
Reasons offered by Report a problem.
TransactionReportReason[]defaultReasons
onReport
Called with the reason picked. Return a promise that resolves with a case number (or nothing); a rejection shows an error banner.
(reason: TransactionReportReason) => Promise<string | void> | void—
onBack
Back button. Hidden when omitted.
() => void—
onCopyReference
Copy the reference, called on a long press of its row.
(reference: string) => void—
locale
BCP 47 locale for amounts and dates. Defaults to the device locale.
string—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Split sheet: the stepper's value rolls and each person's share rolls in the direction of the change; the request button gathers into a spinner and opens into 'Requests sent', then the sheet closes and a footer under Details keeps the result.
  • Report a problem: the banner slides down and pushes the content, then crossfades from sending to sent or error.
  • The category chip and action sheets use their own springs; buttons press on touch down.
  • Reduced motion: digits change in place, sheets fade, the banner fades with an instant height change, buttons crossfade their states.
  • Haptic: select on each step of the people stepper, warning at 2 and 12 (QuantityStepper)
  • Haptic: tap on Split, Attach receipt and the split request; success or error when its promise settles (Button)
  • Haptic: tap when a category or report reason is chosen (ActionSheet)
  • Haptic: success or error when an async Banner action settles (Try again)
  • A static ScrollView; sheets mount only while open, and every animation runs on the UI thread inside the kit components.

Accessibility

  • 'Transaction' is the header; Back is a labelled icon button.
  • The status badge is read by its label (Completed, Pending, Declined, Refunded); a declined amount adds 'not charged' on native.
  • The category chip is a button read as '<category>, category. Change' and opens an action sheet; the change is announced, and so is a rollback when onCategoryChange rejects.
  • Detail rows are read as one element each on native ('Paid with, Everyday •••• 4242'); the reference row has the hint 'Long press to copy' when onCopyReference is set.
  • The split sheet's stepper is adjustable ('People, including you'); the share is plain text that updates with it.
  • The report outcome appears in a banner that is announced, not only in a toast.

Notes for AI

  • Pass transaction.amount signed (negative for spending) with an ISO currency; date is an ISO string or Date. Split is offered only for spending that was not declined.
  • onCategoryChange: the chip changes at once; reject to roll it back. onSplit receives the head count including the payer and each share rounded to cents.
  • onReport resolves with a case number string (shown in the banner) or nothing; a rejection shows an error banner with Try again.
  • hasReceipt switches the receipt button between Attach receipt (shows 'Attached' when the promise resolves) and View receipt; update it from your data after attaching.
  • Render it full-bleed with the navigator header hidden (headerShown: false); it handles the safe areas itself.