Transaction detail screen
ProReact Native · ExpoOne 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.
Installation
$ npx shadcn@latest add @goodui-native-pro/transaction-detail-screenThen 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
- 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.
- 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
| Prop | Type | Default |
|---|---|---|
| transaction | TransactionDetail | defaultTransaction |
| 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. | boolean | false |
| 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.
Related
A banking or wallet home: greeting, the balance of the centered card with hide and show, a swipeable card carousel, quick actions, this month's spending as a scrubbable sparkline and recent transactions, all inside pull to refresh. Swiping to another card rolls the balance, morphs the sparkline and swaps the list.
Settings and navigation cells with a token-colored icon tile, title, subtitle and a trailing value, switch, badge, check or chevron, grouped in inset sections with headers and footers. On touch down the highlight floods in, the tile sinks and the chevron leans forward; on release the highlight lingers and fades.
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.
An iOS-style list of choices that springs up from the bottom edge in grouped cards; it follows the finger to be dragged away, and pulling it up stretches the gap between the cards before they spring back together.
A − value + stepper whose value rolls in the direction of change. Hold a button to repeat with acceleration; the ends push back with a rubber-band bounce and a warning haptic. In a cart the − turns into a trash button at the minimum.
A count, dot or status badge that springs in when it appears, pops when its value changes, caps at 99+ and shrinks away at zero; BadgeAnchor pins it to an icon or avatar.