Skip to content

UX design principles and methodology

This page is the authoritative design reference for all customer-facing screens in the Totara Bank app. Every AI coding agent and human engineer building customer UI must read this page before writing a component. Rules here are prescriptive, not advisory. Where a number is given, use it. Where a token name is given, use it.


Design positioning

We compete directly with Monzo (UK/AU), Starling Bank (UK), and Wise (global). These products have set the quality bar that customers now expect from any digital-native bank. Winning on UX does not mean matching their feature lists — it means delivering a qualitatively better experience in the moments that matter: checking a balance at 6am, sending money in under 30 seconds, understanding a transaction you do not recognise, and trusting that your money is visible and safe at all times.

What those products do well:

Monzo's differentiating characteristic is immediacy. Notifications arrive before the merchant has finished processing. Transaction enrichment is automatic — the user never sees a garbled merchant string. The spending category view gives customers a sense of control without requiring them to manually tag anything. The overall impression is that the app is watching your money for you, not just recording what happened.

Starling's differentiating characteristic is account architecture. Spaces (sub-accounts) let customers segment money mentally without maintaining multiple accounts. The spending insights are merchant-level, not just category-level. The card controls are granular enough to be genuinely useful, not theatrical. The overall impression is a bank that respects that customers think about money in compartments.

Wise's differentiating characteristic is transparency. The exchange rate is shown as the mid-market rate, the fee is shown as a number, and the total cost is shown before the user commits. The rate lock timer communicates exactly how long the rate is valid. The recipient management treats international transfers as a first-class workflow, not an afterthought. The overall impression is that Wise has nothing to hide and is proud of that fact.

What winning looks like for us:

Radical clarity: every number on screen is the right number, fully labelled, formatted consistently, and never obscured by loading or ambiguity. Speed of information: the most important fact on any screen is the first thing rendered, not the last. No friction on common tasks: Pay, Check balance, and View transaction must be reachable in three taps or fewer from any state. Trust communicated through transparency not ceremony: we do not put the customer through confirmation flows to prove we are serious — we show them accurate data and let the data do that work.

The one area where we differentiate from all three competitors is integration: credit, transaction account, and savings in a single account view, informed by real-time profitability data (ROTE), enabling product offers that are personalised to the customer's actual financial relationship with the bank — not a generic upsell. This integration is the commercial reason this wiki exists and the design must support it at every level.


Core design principles

UP-001: Mobile-first, always

Design begins at 390px viewport width and scales upward. Every interaction must be operable with one thumb, with the thumb resting at the bottom of a standard-sized phone. This is not a responsive breakpoint concern — it is a fundamental discipline that affects information hierarchy, tap target sizing, navigation placement, and form layout. If a design requires two hands to operate on a 390px screen, it is not finished.

Back-office mode (tablet and desktop) is a separate layout concern handled by the responsive layer of the design system. The mobile-first constraint applies only to customer mode. But the component library must be built mobile-first: start with the 390px constraint and let the layout system add space at larger breakpoints, never the reverse.

UP-002: Speed over completeness

The most important information on any screen must be rendered first, even if secondary data is still loading. Balance and account information must never be blocked by a spinner waiting for transaction history. Skeleton screens communicate structure while content loads; a full-screen spinner communicates failure. The rule is: if the primary information is available, show it. Load secondary content asynchronously into placeholders.

Perceived performance matters as much as actual performance. An app that shows a skeleton in 100ms and fills in 400ms feels faster than an app that shows nothing for 350ms and then renders everything at once. Design for the skeleton state as a first-class UI state, not as an afterthought.

UP-003: Transparency by default

The customer is entitled to know their balance, every pending transaction, the available balance distinct from the ledger balance, any fees applied, and any holds placed on their account. This information must be surfaced without the customer having to ask for it. Hiding or minimising negative information (overdraft, a declined transaction, a pending hold) is not a UX kindness — it erodes trust faster than any aesthetic shortcoming.

The corollary: when information is withheld for security reasons (full card number, full account number), the act of withholding must be explicit and the path to revealing must be visible. A masked account number with a "tap to reveal" affordance communicates transparency. A masked account number with no obvious reveal communicates concealment.

UP-004: Progressive disclosure

The home screen shows a summary. Tapping opens a detail view. Detail views have expandable sections for technical or regulatory information. This hierarchy must be maintained without exception. The home screen must never show more than: the account balance, a quick-action row, and the last five transactions. Additional information exists — it is one tap away.

Progressive disclosure is not the same as hiding information. The information is available; it is organised so that the customer encounters it at the moment it is relevant to them, not all at once. A transaction list row shows merchant name and amount; tapping shows the bank reference, category, and dispute option. The secondary information was always there — the design just did not surface it until it was needed.

UP-005: Errors are recoverable

Every error state must include: a plain-language explanation of what went wrong, a primary action the user can take to recover, and — where the error is systemic rather than user-caused — an acknowledgement that the bank is aware of the problem. Dead-ends are not acceptable. A screen that says "Something went wrong" with no action button is a design defect, not an edge case.

The error message must be honest about causality. "We could not complete your payment" is better than a generic error code. "Your card was declined by the merchant" is better than "Payment failed." The customer is more likely to take the right action when they understand what happened. If the exact cause is unknown, say so: "We could not confirm whether your payment was received. Check your transaction history before trying again."

UP-006: Confirmation is for irreversible actions only

Confirmation dialogs are a tax on the user's attention. They must be reserved for actions that cannot be undone: sending a payment, closing an account, removing a registered device, initiating a dispute. Confirmation must not appear when: toggling a notification preference, changing a display name, moving money between pots within the same customer account, or any other action that can be reversed in under two taps.

The review screen in the payment flow (step 3 of 5) is a confirmation screen for regulatory purposes. It must not be reduced or skipped. Outside of payments and destructive account actions, default to allowing the action and providing an undo path where technically feasible rather than asking for pre-confirmation.

UP-007: Financial data is sacred

Every amount, balance, account number, and rate displayed in the app is authoritative. The customer will make financial decisions based on what the app shows them. Formatting must be consistent throughout the app: two decimal places always, thousands separator always, currency symbol always on the left, negative amounts expressed with a minus prefix not parentheses. No amount may be truncated, abbreviated, or rounded without explicit disclosure that truncation has occurred.

Rates are especially sensitive. An interest rate shown as "2.5%" when it is "2.50% p.a." misleads the customer. A transfer cost shown as "$3" when it is "$3.00 + a 0.5% margin on the exchange rate" is legally and ethically unacceptable. When in doubt, show more information, not less.

UP-008: Accessibility is not optional

WCAG 2.1 Level AA is the contractual minimum for every screen in the customer app. Colour may never be the sole carrier of information — a red badge must also carry a label or icon that communicates the same information without colour. Touch targets must be at least 44×44pt on iOS and 48×48dp on Android, with at least 8pt of non-interactive space between adjacent targets.

All animations must respect prefers-reduced-motion. Screen readers must be able to navigate every screen in a logical order that matches the visual hierarchy. Custom interactive components (card number reveal, custom keypad, swipe actions) must expose correct ARIA roles and labels. This is not a checklist exercise — it is the baseline expectation for a product used by customers who rely on assistive technology to manage their money.

UP-009: Biometric-first auth

Every return visit to the app must be authenticated by biometric (Face ID, Touch ID, or Android biometric) where the device supports it. Enrolling a biometric is part of the onboarding flow, not an optional post-install step. The customer must be able to reach their home screen in a single biometric gesture. PIN is the explicit fallback when biometric fails or is unavailable; password/email auth is the last resort for account recovery only.

Biometric gates on sensitive actions (reveal card number, initiate payment, change security settings) must confirm the action intent to the customer before triggering the biometric prompt. The prompt must include the action context: "Confirm with Face ID to freeze your Everyday Account" rather than a generic "Authenticate." This prevents confusion when the customer dismisses the prompt and ensures the biometric is meaningfully connected to the action it authorises.

UP-010: Agentic readiness

Every screen built in this app must be buildable, testable, and verifiable by an AI coding agent operating from this documentation alone, without recourse to Figma files, undocumented conventions, or tribal knowledge. This means: every component has a documented name, every state is enumerated (loading, loaded, empty, error), every data shape is defined as a TypeScript interface, and every design token is named in this document or the design system token file. Ambiguity is a defect.

Agentic readiness also means testability. A component that works visually but cannot be addressed by an automated test (because it has no accessible label, no test ID, or its states are driven by undocumented conditions) is incomplete. Document the states. Name the components. Define the data.


Design system foundation

The design system is implemented as a token layer on top of the component library. Components reference tokens only — never hardcoded values. Tokens are defined in the design system package and consumed by every component in the app. The sections below define the token values. Any value not in this document must be added here before it is used in a component.

Typography scale

All sizes are in sp units (scale-independent pixels), which respect the user's system font size preference. Do not use px for type sizes.

Token Size / Line height Weight usage
text-display 32sp / 36sp 700 bold — hero numbers only
text-h1 24sp / 28sp 600 semibold — screen titles
text-h2 20sp / 24sp 600 semibold — section headers
text-h3 17sp / 22sp 500 medium — card titles, list group headers
text-body 15sp / 22sp 400 regular — all body copy
text-body-small 13sp / 18sp 400 regular — secondary labels, metadata
text-caption 11sp / 16sp 400 regular — timestamps, footnotes
text-mono 15sp / 22sp 500 medium — all currency amounts, account numbers, BSB/routing codes

Weight values: 400 (regular), 500 (medium), 600 (semibold), 700 (bold). No other weights are in the type scale.

Rules:

  • Every currency amount in the app uses text-mono. There are no exceptions. The balance hero on the home screen uses text-display weight 700 in the mono font family.
  • Account numbers and BSB/routing codes use text-mono.
  • Never set a body copy element to weight 700 — bold is reserved for hero amounts and display use only.
  • Do not use text-caption for anything the customer needs to act on. Caption is for supplementary information only.

Colour tokens

All colour references in component code must use these semantic token names. No component may reference a hex value, rgb(), hsl(), or a Tailwind palette class directly. Token values are defined in the design system package and map to light/dark mode variants automatically.

Token Semantic meaning
color-primary Brand primary — active navigation, primary buttons, links, focus rings
color-primary-subtle Low-emphasis tint of primary — selected row backgrounds, chip backgrounds
color-surface Page / screen background
color-surface-raised Card surface, slightly elevated from page background
color-surface-overlay Bottom sheet and modal background
color-surface-invert Inverse surface for dark-on-light elements in dark mode
color-text-primary All primary body text
color-text-secondary Secondary labels, metadata, placeholder text
color-text-disabled Disabled input labels and values
color-text-on-primary Text rendered on top of color-primary backgrounds
color-border Default divider and input border
color-border-strong Focused input border, emphasis separators
color-positive Credit amounts, income, positive indicators (growth)
color-positive-subtle Background tint for positive contexts
color-negative Debit amounts, expenses, errors, destructive actions
color-negative-subtle Background tint for negative / error contexts
color-warning Overdraft notice, rate expiry, impending action needed
color-warning-subtle Background tint for warning contexts
color-info Informational banners, pending states, neutral alerts
color-info-subtle Background tint for informational contexts

Both light mode and dark mode mappings are required for every token. A component that only works in light mode is incomplete.

Rules:

  • color-positive and color-negative are never the only means of communicating positive/negative — a label, icon, or sign prefix must also carry the information (UP-008).
  • color-negative on text must meet the WCAG 4.5:1 contrast ratio against its background.
  • Never use color-primary for destructive actions — use color-negative.
  • color-surface-overlay is used only for modal and sheet backgrounds, never for cards.

Spacing scale

Base unit: 4px. All padding, margin, and gap values must be taken from this scale. No magic numbers.

Token Value
space-1 4px
space-2 8px
space-3 12px
space-4 16px
space-5 20px
space-6 24px
space-8 32px
space-10 40px
space-12 48px
space-16 64px

Standard insets for screen content: horizontal space-4 (16px) on mobile. Card internal padding: space-4 (16px) all sides. List row vertical padding: space-3 (12px) top and bottom.

Rules:

  • Never write a spacing value that is not in this scale (e.g. 10px, 14px, 18px). If the design requires a value not in the scale, escalate rather than introduce a magic number.
  • Safe-area insets (notch, home indicator) are applied on top of standard spacing — they do not replace it.

Border radius

Token Value Usage
radius-sm 6px Input fields, small chips, inline badges
radius-md 12px Buttons (all sizes), action sheets rows
radius-lg 20px Cards, bottom sheets, modal containers
radius-full 9999px Pills, avatar containers, toggle tracks

Rules:

  • Cards always use radius-lg. This includes account cards, transaction cards, product offer cards.
  • Buttons always use radius-md regardless of button size.
  • Tags, category chips, and status pills always use radius-full.
  • Never mix radius values on the same card/container.

Elevation and shadow

Elevation communicates Z-axis position. It must only be used to indicate genuine layering — never for decorative purposes.

Token Shadow value Usage
elevation-0 none Flat surface elements, list rows, in-page sections
elevation-1 0 2px 8px rgba(0,0,0,0.06) Cards on page background, bottom nav bar
elevation-2 0 8px 32px rgba(0,0,0,0.12) Bottom sheets, modals, floating toasts

Rules:

  • Never apply elevation-2 to a card on the page background — that combination implies the card is floating above the sheet layer, which it is not.
  • In dark mode, elevation is often better expressed with a surface colour shift rather than a shadow. The token system handles this mapping; do not override it with a hardcoded shadow in dark mode.
  • Never apply elevation to text, icons, or decorative elements.

Motion tokens

Token Value Usage
duration-fast 120ms Micro-interactions: toggle state change, ripple, icon swap
duration-standard 240ms Standard transitions: screen enter, card expand, sheet appear
duration-slow 400ms Complex transitions: onboarding step change, success animation
easing-standard cubic-bezier(0.2, 0, 0, 1) General purpose — elements that are already on screen
easing-enter cubic-bezier(0, 0, 0.2, 1) Elements entering the screen
easing-exit cubic-bezier(0.4, 0, 1, 1) Elements leaving the screen

Rules:

  • No interactive feedback animation (button press, toggle, swipe confirm) may exceed duration-standard (240ms). Slow animations on direct manipulation feel broken.
  • Screen transitions use duration-standard with easing-enter / easing-exit as appropriate.
  • Success states (payment complete, onboarding step complete) may use duration-slow for a single celebratory animation. This must not repeat or loop.
  • All animation must be wrapped in a prefers-reduced-motion check. When prefers-reduced-motion: reduce is set, provide an instant alternative (no transition, no animation). The UI must be fully functional without motion.

Bottom tab bar

The customer app has exactly five tabs in this exact left-to-right order:

  1. Home — balance hero, quick actions, recent transactions
  2. Accounts — list of all accounts and savings pots
  3. Pay — payment entry point (send, request, top-up, transfer)
  4. Cards — virtual and physical card management
  5. More — settings, profile, security, support, documents

Rules:

  • Five tabs. Not four. Not six. The information architecture was designed around this constraint. Adding a sixth tab requires an architecture review.
  • Tab labels are always shown beneath the icon. Icon-only tabs are not permitted (accessibility: icon-only tabs fail WCAG for users who cannot interpret icons without labels).
  • Active tab: icon and label in color-primary. Inactive: icon and label in color-text-secondary.
  • The tab bar uses elevation-1 shadow and sits above the safe-area home indicator. Bottom inset must use SafeAreaInsets.bottom — never a hardcoded pixel value.
  • There are no floating action buttons anywhere in the customer app. All entry points to payment flows are via the Pay tab. This is a deliberate architecture decision to ensure payment flows are always properly authenticated and sequenced.
  • Badge counts on tabs (unread notifications, pending actions) must have an accessible label: "Pay tab, 2 pending actions", not just a red dot.

Use a bottom sheet for: - Quick confirmations (freeze card, cancel a pending payment) - Filter and sort panels - Brief forms with 1–3 fields - Action menus (dispute, categorise, export transaction) - Account number and card number reveal (biometric gated)

Use a full-screen push for: - Multi-step flows (payment send, onboarding, dispute submission) - Document views (statements, terms, letters) - Complex forms (address change, ID verification) - Any flow that has a defined sequence with a back button

Never use a modal, alert, or dialog for: - Error messages — use inline errors or toasts - Informational content that does not require an action - Navigation decisions where a screen would be more appropriate

Bottom sheet height: snap points at 50% and 92% of screen height. Drag handle always visible. Dismissible by drag down or tap on scrim. When a bottom sheet contains a form with a keyboard, it must resize to keep the active field visible above the keyboard — do not allow the keyboard to cover the active input.

Back navigation

Every screen accessible via full-screen push has a back button in the top-left corner rendered as a chevron-left icon. The back button must:

  • Always be present — never removed for "clean design" reasons
  • Be at least 44×44pt touch target
  • Have an accessible label that describes the destination: "Back to Accounts", not just "Back"

Never rely solely on swipe-back for navigation. Swipe-back is a gestural shortcut; it is not the primary back mechanism. Customers using switch control, Voice Control, or single-switch scanning cannot access swipe-back.

Navigation bar layout: - Left: back button (chevron-left + destination label for first-level screens; chevron-left only for deep screens) - Centre: screen title (sentence case, text-h3, color-text-primary) - Right: at most one contextual action. If the action is primary (Save, Done, Next), use a text button in color-primary. If it is a secondary action (share, filter, more options), use an icon button with accessible label.

Never place two icon buttons on the right side of the nav bar. If two actions are needed, put the secondary action in a "More" overflow menu.

Deep linking

Every screen in the customer app must be addressable by a URL so that push notifications, marketing links, and support team deep links can route the customer directly to the relevant context.

Required deep link paths:

Screen Path
Home /home
Account detail /accounts/{account_id}
Transaction detail /accounts/{account_id}/transactions/{transaction_id}
Pay send flow /payments/send
Pay send to recipient /payments/send/{recipient_id}
Card detail /cards/{card_id}
Card freeze /cards/{card_id}/freeze
Cards screen /cards
Statement /accounts/{account_id}/statements/{statement_id}
Notification detail /notifications/{notification_id}
Dispute flow /accounts/{account_id}/transactions/{transaction_id}/dispute

Every deep link handler must handle the case where the linked entity no longer exists (deleted account, expired notification, cancelled payment). The handler must show an appropriate message and offer a navigation path to a valid screen. Crashing or showing a blank screen on an invalid deep link is a defect.


Screen patterns

Home screen

The home screen is the single most important screen in the app. It must render the primary account balance before any other content, using the balance hero component.

Layout (top to bottom):

  1. Greeting row — "Good morning, [first name]" in text-body-small, color-text-secondary. No logo, no date (the phone has a clock).
  2. Balance hero — available balance in text-display weight 700, text-mono, color-text-primary. Label "Available balance" in text-caption, color-text-secondary above the amount. If ledger balance differs from available balance (pending transactions, holds), show both: available balance prominent, ledger balance in text-body-small below.
  3. Overdraft banner — conditional. If account is in debit: display the balance in color-negative. Show a banner in color-warning-subtle immediately below the balance hero with text such as "Using $X of your $Y overdraft facility." This banner must not be dismissible.
  4. Quick action row — four pill buttons in a horizontal scrollable row: Pay, Request, Top Up, Transfer. Icons above labels. Pills use radius-full. This row must not wrap to two lines on 390px.
  5. Recent transactions — section header "Recent" with a "See all" link right-aligned. Last 5 transactions as list rows. Each row: merchant logo (or category icon fallback), merchant name text-body, amount text-mono, date text-caption. Amount colour: neutral on list rows by default (not color-positive/color-negative — that is for detail screens only). Pending transactions: amount in color-text-secondary, "Pending" badge.
  6. Product cards — conditional. If the customer has eligible product offers (personalised by ROTE), show one card below recent transactions. Single card, not a carousel. "See all offers" link if there are more. Never show more than one offer above the fold.

Do not place on the home screen: - News feeds or articles - Marketing banners above the fold (product offers go below recent transactions only) - Multiple account balances — the home screen shows the primary account. The Accounts tab shows all accounts. - Gamification, streaks, or engagement mechanics

Account detail screen

Accessed from the Accounts tab or by tapping the account in the home screen when the customer has a secondary account in context.

Layout:

  1. Nav bar — back button, account name as title, "More" icon (overflow: rename, download statement, close account)
  2. Account header card — account name text-h2, account number masked •••• 1234 with tap-to-reveal affordance, sort code / BSB in text-mono, balance text-h1 text-mono, available balance text-body-small if different. Card uses elevation-1, radius-lg.
  3. Filter chip row — four chips: All, Money in, Money out, Pending. radius-full. Sticks to top of screen on scroll (position: sticky). Active chip: color-primary-subtle background, color-primary label. Inactive: color-surface-raised background.
  4. Transaction list — grouped by date. Date header: text-caption, color-text-secondary. Transaction rows as described in the home screen but with full detail. Infinite scroll, page size 25. Pull-to-refresh at top. Loading indicator at bottom of list while next page fetches (skeleton rows, not a spinner).

No pagination controls. No "Load more" button. The scroll should feel continuous.

Transaction detail screen

Accessed by tapping any transaction row.

Layout:

  1. Merchant block — centred. Merchant logo 64×64pt with radius-lg, or category icon as fallback. Merchant name text-h2, merchant category text-body-small color-text-secondary.
  2. Amount block — centred. Amount in text-display weight 700 text-mono. Status badge: "Completed", "Pending", "Declined", "Refunded" — each with appropriate colour (positive/negative/warning/info). Date and time text-body-small color-text-secondary.
  3. Detail rows — standard list rows with label left and value right:
  4. Reference (customer-provided)
  5. Category (editable — tap opens a category picker sheet)
  6. Payment method (card type + last 4 digits)
  7. Expandable section: "Bank details" — collapsed by default. Contains: bank transaction reference, network reference code, authorisation code. In back-office mode (staff-facing), shows additional fields. Customer mode never shows back-office fields.
  8. Action row — three actions as text buttons: Split, Add note, Dispute. Dispute launches the dispute flow (full-screen push), never routes to email.

Payment send flow

The payment send flow has exactly five steps. No step may be removed or combined with another step. No step may be skipped.

Step 1 — Recipient Recent recipients displayed as horizontally scrollable avatar chips above a search field. Search queries name, account number, and email. "New recipient" option at the bottom of the list. Each recent recipient chip shows initials or avatar, name, and — if a bank transfer — the bank name.

Step 2 — Amount and reference Large numeric keypad (custom, not OS keyboard) occupies the lower half of the screen. Amount displayed in text-display text-mono in the upper half, formatted in real time: $0.00, growing as the customer types. Thousands separator applied at 1,000. Currency selector if the recipient accepts foreign currency. Reference field (text input, standard OS keyboard) above the keypad — tapping it dismisses the keypad and shows the standard keyboard. Reference is required for bank transfers; optional for internal transfers.

Step 3 — Review A read-only summary of every field. Cannot be submitted without the customer having viewed this screen for at least 1 second (implemented as a timed enable on the "Confirm" button — the button is disabled for 1 second to prevent accidental rapid-tap progression). Fields shown: recipient name, recipient account details, amount, currency, reference, fee (if any), estimated arrival time. The "Confirm" button triggers the biometric prompt.

This screen is a regulatory requirement under the bank's payment initiation policy. It must not be reduced, redesigned to feel less like a confirmation, or bypassed for any reason including "returning recipient" status.

Step 4 — Biometric confirmation Biometric prompt with action context: "Confirm payment of $[amount] to [recipient name]." If biometric fails: fallback to PIN entry (max 2 taps to reach PIN fallback). If PIN fails: return to step 3 with an error banner. Never allow the payment to proceed without successful authentication at this step.

Step 5 — Success Full-screen success state. Do not use a toast or banner for payment completion — this is the exception to the toast rule (UP-005 and the toast rule in the interaction patterns section both make this explicit). Show: recipient name, amount, confirmation reference number, estimated arrival time. Share/export button. "Back to home" and "Make another payment" actions. This screen must be reachable from the back stack so the customer can return to it to copy the reference number.

Onboarding flow

Maximum seven screens from app launch to first-transaction-capable state. The flow covers:

  1. Welcome — single screen. Brand mark, brief value proposition (one sentence), "Get started" button. No carousel, no skip.
  2. Phone number — input with country selector. OTP confirmation on next screen (these are two screens — counted as one step in the flow because they are tightly coupled). Standard OS keyboard. Format mask applied as the customer types.
  3. Identity — eIDV handoff — this step hands off to MOD-009 (eIDV & document verification) for document capture and liveness check. Progress indicator shows this is step 3 of the flow. The handoff must be presented as seamless (in-app webview or native SDK, not a browser redirect). On return from the SDK, the app continues to step 4.
  4. Address — address lookup (type to search, powered by address verification service). Manual entry fallback. NZ and AU formats supported.
  5. Selfie liveness — if not captured during eIDV handoff. May be combined with step 3 depending on eIDV SDK capability.
  6. Create PIN — six-digit PIN. Confirmation entry. Custom numeric keypad (not OS keyboard). Biometric enrolment prompt immediately after PIN creation.
  7. Dashboard (pending KYC state) — the customer reaches the home screen immediately after completing onboarding, before KYC is verified. A banner explains that verification is in progress and estimated completion time. All accounts are visible but funding and payment capabilities are restricted until KYC passes. Push notification sent on KYC completion.

Progress indicator: persistent across all onboarding screens, showing step N of M. Every screen has exactly one primary action (forward) and one back action (back button). No walls of text. Regulatory obligations are bulleted; full T&C linked to a document view.

Cards screen

The Cards screen shows the customer's virtual and physical cards.

Layout:

  1. Card component — each card is rendered as a card-shaped component (aspect ratio 1.586:1, the standard card aspect ratio), not as a list row. The card shows: card network logo, card type label ("Virtual card", "Physical card"), last four digits, and the bank name. The full card number, expiry, and CVV are not shown by default.
  2. Reveal controls — "Show card details" button below the card component. Tapping initiates a biometric prompt: "Show card number for [card type] ending [last 4]". On success: the card component animates to show the full number, expiry date, and CVV. The reveal state times out after 30 seconds and returns to masked state. The timer is visible as a countdown.
  3. Freeze toggle — prominent toggle below the card details section. Label: "Card active" / "Card frozen". Toggle background: color-positive when active, color-warning when frozen. Toggling uses an optimistic update — the UI updates immediately and reverts with a toast on API failure.
  4. Spend controls — three toggles in a grouped section: "Online payments", "Contactless payments", "ATM withdrawals". Same optimistic update pattern.
  5. Wallet enrolment — Apple Pay or Google Pay enrolment CTA depending on platform. If already enrolled: show the wallet badge.
  6. Card actions — at the bottom: "Report card lost or stolen" (destructive — color-negative), "Order replacement card" (if physical card).

Settings and profile

Rendered as a grouped list (standard iOS/Android settings pattern).

Sections (in order):

  1. Profile — display name, email address, phone number. Each row opens an edit flow.
  2. Notifications — toggle rows for each notification category (payments, statements, marketing). No nested navigation — toggles are inline.
  3. Security — change PIN, manage biometrics, connected devices, app permissions.
  4. Documents — statements, tax certificates, correspondence. Each opens a document viewer (full-screen push).
  5. Support — in-app chat, FAQs, contact details.
  6. About — version number, privacy policy, terms of service.
  7. Destructive actions — at the bottom, separated by a space-8 gap from the above sections: "Remove this device" and "Close account". Both in color-negative. Both require confirmation (bottom sheet with explicit destructive confirm button).

Automatic payments screen

Automatic payments is a unified view of everything leaving the customer's account on a schedule — standing orders, automatic payments (AP), and direct debit mandates. These are three distinct constructs with different characteristics; the UI must reflect that distinction clearly while presenting them in a single, coherent management surface.

Entry point: Pay tab → "Automatic payments" — a persistent row above the recent payees list, never buried in settings.

List view — three grouped sections:

  1. Standing orders — bank-initiated, customer-defined, fixed amount, fixed schedule. Created and cancelled by the customer inside the app.
  2. Automatic payments — bank-initiated, customer-defined, fixed or variable amount with a defined rule (e.g. pay full balance, pay minimum, pay fixed amount). Primarily used for credit products.
  3. Direct debits — third-party-initiated, authorised by the customer. The bank executes on instruction from the biller. Customer cannot edit the amount — only suspend or cancel.

Each section shows a row per active instruction. If a section has no active instructions, show a designed empty state with a "Set up" CTA — do not collapse the section header. This allows customers to discover the capability even when unused.

List row — per instruction: - Left: biller/payee name (or "You" for internal transfers) - Sub-label: schedule summary — "Every month on the 15th · $250.00" or "On demand · Up to $500.00" - Right: next payment date + amount (if deterministic), or "Variable" if amount is biller-determined - Status badge: Active (default, no badge shown), Suspended (amber), Failed (red), Cancelled (greyed out — shown for 30 days after cancellation for reference)

Instruction detail screen (full-screen push):

Standing order detail: - Payee name, account number (masked) - Amount, frequency, start date, end date (if set), reference - Next payment date (highlighted if within 3 days) - Payment history (last 6 payments, expandable to full history) - Actions: Edit amount / frequency / reference, Suspend, Cancel - Suspended state: "Resume" replaces "Suspend"

Direct debit mandate detail: - Biller name, biller logo (fallback: category icon) - Mandate reference (DDR number) - Authorisation date - Last payment date + amount - Next expected payment (if available from biller data) - Payment history (last 6, expandable) - Actions: Cancel mandate only — no edit (amount/timing is biller-controlled). Add a clear explainer: "The amount and timing of this direct debit is set by [biller name]. To change these, contact [biller name] directly."

Automatic payment rule detail: - Linked account (e.g. "Totara Everyday Visa ending 4321") - Rule type: pay full balance / pay minimum / pay fixed amount - Payment date: day of month - Funding account - History - Actions: Edit rule, Cancel

Creating a standing order — 4-step flow:

Step 1: Recipient (same recipient picker as payment send flow — reuse component). Step 2: Amount + frequency. Frequency options: Weekly / Fortnightly / Monthly / Quarterly / Annually. Start date picker. Optional end date. Reference field. Step 3: Review — all fields, next payment date calculated and shown explicitly, funding account confirmed. Step 4: Confirm — biometric gate (same pattern as payment send). Success screen with next payment date prominent.

Failure handling:

A failed instruction (insufficient funds, account closed, mandate error) must surface immediately: - Push notification on the day of failure: "Your [standing order / direct debit] to [payee] of $[amount] could not be processed." Deep link to the instruction detail screen. - In the list: row shows Failed badge, sub-label shows "Failed [date] · Insufficient funds" or equivalent. - On the detail screen: a banner at the top (not a modal): "Payment failed on [date]. [Reason]. [Action button]". Action depends on instruction type — for standing orders: "Retry now" or "Edit". For direct debits: "The biller may retry — contact [biller] if this is urgent."

Upcoming payments widget:

On the home screen, a collapsible card shows upcoming automatic payments within the next 7 days. Shows max 3 rows. "See all" links to the full Automatic payments screen. If no payments due in 7 days, the card does not appear (unlike empty states on other screens — this is space-critical on the home screen).

AI agent rules specific to this screen:

  • Standing orders and automatic payments are written by the customer (bank-initiated) — they call the /payments/standing-orders API. Direct debit mandates are read-only from the customer's perspective — sourced from MOD-114 (direct debit mandate management). These are different API endpoints with different data shapes. Never conflate them in a single component.
  • The "Cancel mandate" action for a direct debit must display a confirmation bottom sheet that explicitly states: "This will stop future payments to [biller]. [Biller] may still attempt to collect — contact them to avoid any service interruption." This is a regulatory disclosure, not optional UX copy.
  • A failed payment row must not be removed from the list when the customer views it. It must remain visible until either the instruction is retried successfully or cancelled.

Interaction patterns

Amounts and number entry

Currency amounts are never entered using the operating system's native keyboard. All currency amount inputs use a custom numeric keypad with the following characteristics:

  • Keys: 0–9, decimal point, delete
  • Layout: 3×4 grid (standard phone keypad layout)
  • Key size: minimum 64×64pt
  • Amount display above the keypad: text-display weight 700 text-mono
  • Format applied in real time: thousands separator inserted automatically, decimal point permitted once, maximum two digits after decimal
  • Currency symbol displayed to the left of the amount at all times: $ for NZD and AUD, appropriate symbol for other currencies
  • For international transfers: the local currency amount and the converted amount are shown simultaneously, with the exchange rate in text-caption below

The OS keyboard is used for: names, references, search queries, email addresses, and other text fields. Never use the OS numeric keyboard for currency amounts — it does not provide the custom format masking required by UP-007.

Swipe actions on list rows

Transaction list rows support a left-swipe gesture to reveal contextual actions.

Left swipe (from right edge): - Dispute (shown for completed transactions) - Block merchant (shown for all transactions) - Edit category (shown for all transactions)

Right swipe: reserved for navigation back. Never assign an action to right swipe on a list row — it conflicts with the navigation back gesture.

Rules:

  • Swipe threshold: 80px before the action panel is revealed. Below this threshold, releasing the row snaps it back.
  • Action trigger threshold: 160px (the full action panel width). If the customer releases beyond this threshold, the primary action triggers automatically with a haptic confirmation feedback.
  • For destructive actions revealed by swipe (dispute is not destructive; blocking a merchant is borderline): show a confirmation bottom sheet before executing.
  • The swipe panel must not activate during vertical scroll. Use velocity direction to disambiguate horizontal vs. vertical gestures.

Pull to refresh

Pull-to-refresh is available on all list and feed screens: transaction lists, account lists, notification lists.

  • Custom refresh indicator — not the OS default spinner. Use an animated version of the brand mark or a circular progress ring in color-primary.
  • Refresh is triggered at a drag distance of 80pt from the top of the scroll view.
  • On completion: the list updates in place. If no new data: show no feedback beyond a timestamp update.
  • Last-refreshed timestamp displayed in text-caption color-text-secondary at the top of the content area when data is more than 5 minutes old.
  • Never show stale transaction data without indicating when it was last refreshed.

Haptic feedback

Event Pattern
Payment complete Heavy impact
Successful action (card freeze, toggle change) Medium impact
Error / failure Notification error pattern
Biometric prompt shown Selection feedback
Swipe action triggered Light impact

Rules:

  • Never trigger haptic on scroll events.
  • Never trigger haptic on passive state changes (balance update, notification arrival — the push notification system handles that).
  • Haptic feedback must be suppressible by the user's system accessibility settings — use the platform's standard haptic API, which respects system settings automatically. Never implement a custom vibration that bypasses accessibility settings.

Loading states

Skeleton screens are the default loading state for any screen or component that fetches data. A skeleton screen renders grey placeholder shapes in the approximate geometry of the content it is waiting for. Skeletons:

  • Use color-surface-raised for the placeholder shape
  • Animate with a shimmer (gradient sweep) using duration-slow at reduced opacity
  • Match the approximate dimensions of the actual content (a transaction row skeleton is the same height as a transaction row)
  • Shimmer animation must respect prefers-reduced-motion — when reduced motion is set, skeletons are static with no animation

Full-screen spinner — used only for: initial app authentication, biometric prompt processing. Not used for data loading on already-authenticated screens.

Optimistic updates — for toggle interactions (card freeze, notification settings, spend controls): the UI updates immediately on tap. The API call proceeds in background. On failure: revert the toggle to its previous state and show an error toast: "Could not update card freeze. Try again." The toast auto-dismisses after 3s because the failure is informational and the original state is restored.

Empty states

Every list component must have a designed empty state. Plain "No results" text is not an empty state — it is an absence of design.

Empty state structure: - Illustration or icon (simple, not photographic) — decorative, aria-hidden - Heading text-h3 — plain language explanation - Body text-body color-text-secondary — one or two sentences of context - Primary action button — what the customer should do next

Examples:

Screen Heading Body Action
Transaction list (new account) No transactions yet Your transactions will appear here once you make your first payment. Make a payment
Notification list You are all caught up No new notifications. Go to home
Saved recipients No saved recipients People you pay regularly will appear here for quick access. Send a payment

Toasts

Toasts slide up from the bottom of the screen, above the tab bar.

  • Success / info toasts: auto-dismiss after 3 seconds. Green left border for success, blue for info.
  • Error toasts: do not auto-dismiss. Require explicit tap to dismiss. Red left border. Error toasts include a brief action label if a retry is possible: "Could not load transactions. Retry."
  • Maximum one toast at a time. If a second toast is triggered while one is visible, queue it — do not stack.
  • Payment success is never a toast. Payment completion always uses the full success screen (step 5 of the payment flow). A payment notification arriving in the background (for a received payment) may use a toast.
  • Toast copy: sentence case, no full stop. Maximum 80 characters.

Financial data formatting

The following rules are mandatory for every component in the app that displays financial data. They are not stylistic preferences.

Amounts

  • Always 2 decimal places: $1,234.56. Never $1,234.6 or $1,235.
  • Always thousands separator: $1,234.56. Never $1234.56.
  • Currency symbol always left-aligned, immediately adjacent to the amount: $1,234.56. Never 1,234.56 NZD on a customer-facing display (ISO code format is acceptable in compact metadata contexts only).
  • Negative amounts: minus prefix, color-negative. −$150.00. Never ($150.00).
  • Zero: $0.00. Never $0 or $—.

Credit and debit colour

  • On transaction detail screens: credit amounts (money in) are displayed in color-positive. Debit amounts are displayed in color-negative.
  • On list rows (home screen recent transactions, account transaction list): amounts are displayed in color-text-primary by default. Colour-coding in list rows is reserved for pending items (color-text-secondary) and is otherwise neutral, because a feed of green and red numbers creates visual noise.
  • On balance displays: balances are color-text-primary unless the account is in debit, in which case the balance is color-negative.

Pending transactions

  • Amount in color-text-secondary (lighter than confirmed)
  • "Pending" status badge in color-info
  • Amount shown in text-body weight 400 (not medium or semibold as completed transactions may be)

Balances

  • Always show available balance as the primary balance figure. If the available balance is less than the ledger balance (due to pending debits or holds), show both:
  • Available: $2,340.00 (primary, large)
  • Balance: $2,490.00 (secondary, smaller, with "includes $150.00 pending" explainer)
  • Never show only the ledger balance without making clear it is not the available amount.

Account numbers

  • Default: masked to last 4 digits: •••• 1234. Four bullet points, two spaces, four digits. All in text-mono.
  • Full account number: available after biometric gate. Reveal with a tap-to-copy affordance.
  • BSB (NZ) and routing codes (AU): displayed in text-mono with space separator for readability: 12-3456 (BSB format) or 062-000 (routing).

Dates and times

Age Format Example
Today "Today" + time Today at 2:34 PM
Yesterday "Yesterday" + time Yesterday at 9:12 AM
Within 7 days Day name + time Monday at 4:55 PM
This calendar year Day + month 14 March
Previous years Day + month + year 14 March 2024

Transaction detail always shows full date and time regardless of age.

Interest rates

  • Always 2 decimal places: 2.50% p.a.. Never 2.5% or 2.5% per annum (use the abbreviation).
  • Never round an interest rate: 1.75% p.a. not 1.8% p.a. or 2% p.a..
  • For comparison displays (product offers): show the rate prominently and the basis (p.a.) clearly adjacent.

Exchange rates

  • Show the mid-market rate and the total fee separately. Never bundle them.
  • Format: 1 NZD = 0.6124 USD (6 significant figures for the FX rate).
  • When a rate lock is active: show the lock expiry timer in minutes and seconds.
  • Total cost breakdown: $1,000.00 NZD → $612.40 USD with fee shown as Fee: $3.50 NZD.

Accessibility requirements

Colour and contrast

  • WCAG 2.1 Level AA minimum throughout. Target AAA for financial data (amounts, balances, rates).
  • Text on surface contrast: ≥ 4.5:1 for text-body and smaller. ≥ 3:1 for text-h1, text-h2, and text-display (≥ 18pt).
  • UI components (buttons, input borders, toggle tracks in their resting state) ≥ 3:1 against adjacent colour.
  • color-positive and color-negative text must meet 4.5:1 contrast on color-surface and color-surface-raised in both light and dark mode. Verify this on every design system token change.
  • Never use colour as the sole indicator of state. A freeze toggle in red must also have a "Frozen" label. A pending transaction in grey must also have a "Pending" badge.

Touch targets

  • Minimum touch target: 44×44pt on iOS, 48×48dp on Android.
  • Minimum spacing between adjacent interactive targets: 8pt.
  • For inline text links (rare — prefer buttons for primary actions): the tap target extends beyond the visual link extent to meet the 44pt minimum.
  • This requirement applies to all interactive elements including: tab bar items, swipe action buttons, chip filters, toggle switches, and icon buttons in nav bars.

Screen readers

  • All icons that convey information must have an accessible label. Icons that are purely decorative (illustrative background elements, separator icons) must be marked aria-hidden="true" or equivalent.
  • Transaction amount read by VoiceOver / TalkBack: "[amount] [currency] [credit or debit] from [merchant] on [date]". Example: "One hundred and fifty dollars New Zealand dollars debit from Countdown on Monday."
  • Balance hero read: "Available balance, two thousand three hundred and forty dollars."
  • Custom components (card number reveal, custom keypad, swipe action rows) must declare the correct ARIA role, state, and label. This must be verified with an actual screen reader — visual inspection is not sufficient.
  • Screen reader navigation order must match the visual top-to-bottom, left-to-right order. When a dynamic component updates (balance refreshes, pending badge appears), the update must be announced via a live region if it is information the customer needs to act on.

Focus management

  • Focus trap: when a bottom sheet or modal opens, focus must move to the first interactive element within it. Focus must not escape the modal while it is open.
  • Focus return: when a modal or sheet is dismissed, focus must return to the element that triggered it.
  • For multi-step flows (payment send): focus moves to the heading of the new step on each step transition.
  • Do not outline: none without providing a custom focus indicator. Every interactive element must have a visible focus ring in keyboard / switch control navigation contexts.

Zoom and text scaling

  • Never set user-scalable=no in the viewport meta tag.
  • The app must be usable at 200% system text scale without content being clipped or UI elements overlapping. Test at large accessibility font sizes.
  • Do not use fixed pixel heights on list rows — they must expand to accommodate larger text.

Form fields

  • Every form field must have a visible label. Placeholder text is not a label. The label must remain visible when the field is focused and when it contains a value.
  • Error messages must be associated with their field (aria-describedby or equivalent) so that screen readers announce the error when the field receives focus.
  • Required fields must be indicated — not only by an asterisk (which has no accessible meaning by default), but by explicit label text ("Required") or aria-required.

Reduced motion

All transitions and animations must be wrapped in a prefers-reduced-motion check:

@media (prefers-reduced-motion: reduce) {
  /* Provide instant alternative */
}

This includes: skeleton shimmer animation, screen transitions, toast slide-in, success animation, card reveal animation, swipe action reveal. When reduced motion is active, transitions should be instant (0ms or near-instant) rather than simply slowed down.


Competitive analysis and benchmarks

The following table summarises how the key competitors approach each interaction domain, what they do well, and the standard we must match or exceed.

Domain Monzo Starling Wise Our target
Home screen information density Balance hero, spending summary ring, recent transactions with merchant enrichment. Excellent hierarchy. Balance hero, quick actions, recent transactions with space indicators. Clean, uncluttered. Multi-currency balance view — each currency as a row. Dense but scannable. Balance hero first, always. Four quick actions. Five recent transactions. No secondary marketing above the fold. Match Monzo's enrichment, Starling's clarity.
Payment flow depth 4 steps (recipient, amount, review, confirm). Fast for returning recipients but still enforces review. 4 steps. Very similar to Monzo. Slightly better international payment UX. 6+ steps for international, driven by currency/route selection. Transparent throughout. 5 steps always (regulatory review screen is mandatory). Fast for domestic, comprehensive for international. Never skip the review screen.
Transaction search Full-text search across merchant name, amount, category. Fast. Full-text search with date range filter. Comprehensive. Basic search by recipient. Less mature. Full-text search (merchant, amount, reference, category). Date range filter. Category filter. Must match or beat Monzo.
Card management Virtual card with reveal, freeze toggle, spend limits. Apple Pay enrolment. Physical + virtual, spend limits by category, freeze, Apple Pay. Virtual Wise card with detailed spend controls. Strong for international use. Freeze, spend controls (3 toggles), reveal with biometric gate, Apple Pay / Google Pay. Match Starling's spend control granularity.
Onboarding length ~5 minutes to transact-capable. Good KYC handoff to async completion. ~5 minutes, similar to Monzo. Slightly less friction on selfie step. ~3 minutes to add money, full verification can be async. Best-in-class speed. 7 screens maximum. KYC async — customer reaches home screen before verification completes. Target: first payment capable within 5 minutes of app launch. Match Wise's async KYC approach.
Notification design Real-time spend notification with merchant logo, amount, remaining daily balance. Industry benchmark. Real-time notifications with merchant name. Slightly less enriched than Monzo. Notifications on transfer status changes, rate lock expiry. Functional, not enriched. Real-time spend notification with: merchant logo, merchant name (enriched), amount, available balance after transaction. Match Monzo.

Monzo patterns to match or exceed

  • Real-time spend notifications: the notification must arrive before the merchant terminal has printed the receipt. This is a latency and infrastructure requirement as much as a UX requirement — see SD03 (AML Monitoring) for the transaction event pipeline.
  • Merchant enrichment: garbled merchant strings from the payment network (e.g. "AMZN Mktp UK*ABC123") must be resolved to a clean merchant name and logo before the transaction appears in the app. This requires a merchant enrichment service.
  • Spending categories: transactions are automatically categorised. Categories must be editable by the customer. A visual breakdown of spending by category must be available (not on the home screen — one tap away via the Accounts or Pay tab).
  • Pot / saving separation: Monzo Pots show visually and behaviourally as separate from the main balance. Our equivalent (savings accounts, sub-accounts) must follow the same mental model: distinct balance, distinct history, transfers between them are visible and deliberate.

Starling patterns to match or exceed

  • Spaces: Starling Spaces are sub-accounts that feel like separate pots of money. Our account architecture must support named sub-accounts with their own balance display, filterable transaction history, and direct transfer path from the main account.
  • Round-up savings: automatic round-up of transactions to the nearest dollar, saved to a designated pot. This is a feature requirement, not a UX requirement — but the UX must surface it clearly during onboarding and in settings without burying it in a submenu.
  • Spending insights: Starling surfaces merchant-level spending insights, not just category totals. "You spent $340 at Countdown this month" is more useful than "You spent $600 on groceries." Our spending insights must match this level of specificity.
  • No-fees abroad messaging: Starling is clear about what is free internationally. We must similarly make our fee structure (or lack thereof for AU↔NZ transfers within the product) explicit in the UI, not hidden in T&C.

Wise patterns to match or exceed

  • Multi-currency balance display: customers holding NZD and AUD balances must be able to see both on the accounts screen in a single glance. The Wise model — one row per currency with balance and flag — is the reference. Our implementation adds the integrated credit/savings layer.
  • Mid-market rate transparency: when the customer initiates a cross-currency transfer, the exchange rate is shown as the mid-market reference rate. Our margin or fee is shown separately, as a specific dollar amount, before the customer commits.
  • Rate lock timer: if the customer is shown a rate that is locked for a period (even if briefly for the duration of a payment session), the lock expiry must be displayed as a countdown timer. The customer must be informed if the rate expired before they confirmed.
  • Transfer cost upfront: the total cost of a transfer (fee + exchange rate spread) is shown on the amount entry screen before the customer reaches the review screen. Not just in the fine print. This is a commercial differentiator that builds trust.
  • Recipient management: international recipient details (IBAN, SWIFT/BIC, routing numbers) are saved and managed as first-class entities, not as free-text fields re-entered each time.

Our differentiator: integrated financial view

None of the three competitors offers an integrated view of credit, transaction account, and savings in a single account summary. Monzo has current + savings in one app; Starling has current + savings + overdraft; Wise is primarily a FX/remittance product. None offers a single account view that shows:

  • Current account balance and recent transactions
  • Credit facility utilisation and available credit
  • Savings balance and accrued interest in the same account view
  • A real-time product offer based on the customer's actual profitability to the bank (ROTE-informed)

The home screen and account detail screen are designed to surface this integration without overwhelming the customer. The home screen shows one primary balance (the current/transaction account). The Accounts tab shows the full picture. Product offers are personalised and shown below the fold on the home screen, with no more than one visible at a time. The design must make integration feel effortless rather than complicated.


AI agent implementation rules

The following rules are mandatory for any AI coding agent implementing customer-facing UI in this app. These rules exist because: (a) financial data handling has no tolerance for formatting shortcuts, (b) accessibility failures in production are regulatory and reputational risks, and (c) the codebase is built and maintained by agents as much as humans — consistency requires machine-readable rules, not vague guidelines.

  1. Every component that displays a financial amount must accept an explicit currency: 'NZD' | 'AUD' | string prop. The currency symbol, thousands separator, and decimal format must be derived from this prop using the shared formatCurrency utility. Never hardcode $ or any currency symbol in a component.

  2. Never use any as a TypeScript type for financial data structures. The following interfaces are canonical and must be used without modification. If a new field is needed, extend the interface and update this documentation. Canonical interfaces:

interface Money {
  amount: number;          // integer cents — never floating point
  currency: 'NZD' | 'AUD' | string;
}

interface Balance {
  available: Money;
  ledger: Money;
  currency: string;
}

interface Transaction {
  id: string;
  accountId: string;
  amount: Money;           // negative for debits
  type: 'debit' | 'credit';
  status: 'pending' | 'completed' | 'declined' | 'refunded';
  merchant: MerchantInfo;
  reference: string | null;
  category: TransactionCategory | null;
  authorisedAt: string;    // ISO 8601
  settledAt: string | null;
}

interface Account {
  id: string;
  name: string;
  accountNumber: string;   // full, stored server-side — display masked
  bsb: string | null;      // NZ
  routingCode: string | null; // AU
  balance: Balance;
  type: 'transaction' | 'savings' | 'credit';
  status: 'active' | 'suspended' | 'closed';
}

interface Card {
  id: string;
  accountId: string;
  last4: string;
  type: 'virtual' | 'physical';
  network: 'visa' | 'mastercard';
  status: 'active' | 'frozen' | 'cancelled';
  expiryMonth: number;
  expiryYear: number;
  controls: CardControls;
}

interface Payment {
  id: string;
  fromAccountId: string;
  recipient: RecipientSummary;
  amount: Money;
  reference: string;
  status: 'draft' | 'pending_auth' | 'submitted' | 'processing' | 'completed' | 'failed';
  fee: Money | null;
  estimatedArrival: string | null;  // ISO 8601
  confirmedAt: string | null;
}
  1. All components that fetch data must implement three render paths without exception:
  2. Loading: skeleton screen matching the approximate geometry of the loaded content. Never a spinner for list or card content.
  3. Loaded: the component renders correctly with valid data.
  4. Error: an inline error message with a retry action. The error message must be specific enough for the customer to understand what failed. Map API error codes to user-facing messages using the centralised error catalogue (src/errors/catalogue.ts). Never render "Something went wrong" as the final user-facing message.

  5. Colour must come from design tokens only. No hex values (#1A1A2E), no rgb(), no hsl(), no CSS custom properties that are not design token references, and no Tailwind colour palette classes that bypass the token system (text-red-500, bg-blue-200). Use the token classes or CSS variables defined in the design system package. A CI lint rule enforces this — do not attempt to suppress the lint warning.

  6. Every interactive element must have an accessible label that describes both the action and the context. Generic labels are defects:

  7. Correct: aria-label="Freeze Everyday Account ending 4321"
  8. Incorrect: aria-label="Freeze"
  9. Correct: aria-label="View transaction: $45.00 at Countdown on Monday"
  10. Incorrect: aria-label="View transaction"

  11. Amount formatting must use the shared utility function formatCurrency(amount: Money, options?: FormatCurrencyOptions): string. This function handles: currency symbol, thousands separator, decimal places, sign, and currency-specific formatting rules. Never implement amount formatting inline in a component. The function is located at src/utils/formatCurrency.ts.

  12. All forms must handle field-level validation errors returned from the API. The errors object from the API response maps field names to error codes. These codes map to user-facing messages in src/errors/catalogue.ts. Each error message must appear adjacent to the field it relates to (not in a summary banner at the top of the form). The field must enter its error state (border colour color-negative, error label visible) immediately on API error return, not only on re-submission.

  13. Any screen that receives a deep link must handle the entity-not-found case. If the linked entity (account, transaction, card, payment) has been deleted, cancelled, or does not belong to the authenticated customer: show a screen-level error with a brief explanation and a navigation action back to a safe context (e.g. "This transaction is no longer available. Go to Accounts."). Never navigate to a blank screen or throw an unhandled error.

  14. Biometric authentication must always have a path to PIN fallback. The PIN fallback must be reachable within two taps from any biometric failure or cancellation state. The PIN entry screen must be the same component regardless of which action triggered the biometric prompt — do not implement separate PIN flows for payment confirmation, card reveal, and account number reveal. Reuse the single BiometricGate component.

  15. The five-step payment send flow must be implemented in full without shortcutting any step. The review screen (step 3) is a regulatory requirement under the bank's payment initiation policy. Even for returning recipients whose details are pre-populated, the review screen must be displayed and must be visible for at least 1 second before the confirm action is enabled. Shortcutting this step is a compliance defect, not a UX improvement.

  16. Never store the following data in component state, React context, localStorage, sessionStorage, or browser cookies: full card number, CVV, card expiry (in combination with number), full account number in unmasked form, or authentication tokens beyond what is required for the active session. Security-sensitive data must be retrieved fresh from the API on each display event and held only in the transient render output for the duration of the reveal timeout (30 seconds for card details). The biometric gate must re-authorise on each reveal, not cache the authorisation.

  17. All list components that can contain more than 50 items must implement virtual rendering (windowed list). Use the project's standard virtual list component (VirtualList from the design system package). Rendering unbounded lists causes frame drops on mid-range devices. The following screens require virtual rendering by definition: transaction list (account detail), notification list, recipient search results.

  18. Form inputs for structured financial identifiers must apply format masks as the customer types, not post-submission:

    • BSB (NZ): XX-XXXX (2 digits, hyphen, 4 digits)
    • BSB (AU routing): XXX-XXX (3 digits, hyphen, 3 digits)
    • Account number: no mask beyond limiting to numeric input and the maximum length for the relevant bank
    • Card number: XXXX XXXX XXXX XXXX (groups of 4 with double-space separator)
    • Expiry: MM/YY
  19. Push notification tap handlers must route to the correct deep link for the notification type. Routing a payment notification to the home screen instead of the relevant transaction or payment detail screen is a defect. Every notification type must have a documented deep link target. The mapping is defined in src/notifications/routes.ts. When adding a new notification type, add the route mapping before shipping.

  20. The app must render correctly within iOS safe-area insets (notch, Dynamic Island, home indicator) and Android edge-to-edge mode. Use the SafeAreaContext provider from react-native-safe-area-context. Never hardcode top or bottom padding to accommodate the status bar or home indicator. Test on: iPhone 15 Pro (Dynamic Island), iPhone SE (home button), a foldable Android device, and a standard Android device with gesture navigation. Safe-area insets must be applied to: the tab bar bottom, the screen header top, full-screen modals, bottom sheets, and the custom keypad container.


  • Frontend architecture: architecture/frontend-architecture.md
  • Customer-driven architecture principle AP-005: architecture/AP-005-customer-driven.md
  • App system domain: SD08 (docs/systems/SD08-app/index.md)
  • MOD-068 customer authentication and biometrics
  • MOD-073 document delivery and PDF statements