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 usestext-displayweight 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-captionfor 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-positiveandcolor-negativeare never the only means of communicating positive/negative — a label, icon, or sign prefix must also carry the information (UP-008).color-negativeon text must meet the WCAG 4.5:1 contrast ratio against its background.- Never use
color-primaryfor destructive actions — usecolor-negative. color-surface-overlayis 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-mdregardless 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-2to 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-standardwitheasing-enter/easing-exitas appropriate. - Success states (payment complete, onboarding step complete) may use
duration-slowfor a single celebratory animation. This must not repeat or loop. - All animation must be wrapped in a
prefers-reduced-motioncheck. Whenprefers-reduced-motion: reduceis set, provide an instant alternative (no transition, no animation). The UI must be fully functional without motion.
Navigation architecture¶
Bottom tab bar¶
The customer app has exactly five tabs in this exact left-to-right order:
- Home — balance hero, quick actions, recent transactions
- Accounts — list of all accounts and savings pots
- Pay — payment entry point (send, request, top-up, transfer)
- Cards — virtual and physical card management
- 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 incolor-text-secondary. - The tab bar uses
elevation-1shadow and sits above the safe-area home indicator. Bottom inset must useSafeAreaInsets.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.
Modal sheets vs. full-screen push¶
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):
- Greeting row — "Good morning, [first name]" in
text-body-small,color-text-secondary. No logo, no date (the phone has a clock). - Balance hero — available balance in
text-displayweight 700,text-mono,color-text-primary. Label "Available balance" intext-caption,color-text-secondaryabove the amount. If ledger balance differs from available balance (pending transactions, holds), show both: available balance prominent, ledger balance intext-body-smallbelow. - Overdraft banner — conditional. If account is in debit: display the balance in
color-negative. Show a banner incolor-warning-subtleimmediately below the balance hero with text such as "Using $X of your $Y overdraft facility." This banner must not be dismissible. - 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. - 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, amounttext-mono, datetext-caption. Amount colour: neutral on list rows by default (notcolor-positive/color-negative— that is for detail screens only). Pending transactions: amount incolor-text-secondary, "Pending" badge. - 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:
- Nav bar — back button, account name as title, "More" icon (overflow: rename, download statement, close account)
- Account header card — account name
text-h2, account number masked•••• 1234with tap-to-reveal affordance, sort code / BSB intext-mono, balancetext-h1text-mono, available balancetext-body-smallif different. Card useselevation-1,radius-lg. - 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-subtlebackground,color-primarylabel. Inactive:color-surface-raisedbackground. - 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:
- Merchant block — centred. Merchant logo 64×64pt with
radius-lg, or category icon as fallback. Merchant nametext-h2, merchant categorytext-body-smallcolor-text-secondary. - Amount block — centred. Amount in
text-displayweight 700text-mono. Status badge: "Completed", "Pending", "Declined", "Refunded" — each with appropriate colour (positive/negative/warning/info). Date and timetext-body-smallcolor-text-secondary. - Detail rows — standard list rows with label left and value right:
- Reference (customer-provided)
- Category (editable — tap opens a category picker sheet)
- Payment method (card type + last 4 digits)
- 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.
- 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:
- Welcome — single screen. Brand mark, brief value proposition (one sentence), "Get started" button. No carousel, no skip.
- 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.
- 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.
- Address — address lookup (type to search, powered by address verification service). Manual entry fallback. NZ and AU formats supported.
- Selfie liveness — if not captured during eIDV handoff. May be combined with step 3 depending on eIDV SDK capability.
- Create PIN — six-digit PIN. Confirmation entry. Custom numeric keypad (not OS keyboard). Biometric enrolment prompt immediately after PIN creation.
- 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:
- 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.
- 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.
- Freeze toggle — prominent toggle below the card details section. Label: "Card active" / "Card frozen". Toggle background:
color-positivewhen active,color-warningwhen frozen. Toggling uses an optimistic update — the UI updates immediately and reverts with a toast on API failure. - Spend controls — three toggles in a grouped section: "Online payments", "Contactless payments", "ATM withdrawals". Same optimistic update pattern.
- Wallet enrolment — Apple Pay or Google Pay enrolment CTA depending on platform. If already enrolled: show the wallet badge.
- 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):
- Profile — display name, email address, phone number. Each row opens an edit flow.
- Notifications — toggle rows for each notification category (payments, statements, marketing). No nested navigation — toggles are inline.
- Security — change PIN, manage biometrics, connected devices, app permissions.
- Documents — statements, tax certificates, correspondence. Each opens a document viewer (full-screen push).
- Support — in-app chat, FAQs, contact details.
- About — version number, privacy policy, terms of service.
- Destructive actions — at the bottom, separated by a
space-8gap from the above sections: "Remove this device" and "Close account". Both incolor-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:
- Standing orders — bank-initiated, customer-defined, fixed amount, fixed schedule. Created and cancelled by the customer inside the app.
- 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.
- 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-ordersAPI. 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-displayweight 700text-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-captionbelow
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-captioncolor-text-secondaryat 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-raisedfor the placeholder shape - Animate with a shimmer (gradient sweep) using
duration-slowat 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.6or$1,235. - Always thousands separator:
$1,234.56. Never$1234.56. - Currency symbol always left-aligned, immediately adjacent to the amount:
$1,234.56. Never1,234.56 NZDon 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$0or$—.
Credit and debit colour¶
- On transaction detail screens: credit amounts (money in) are displayed in
color-positive. Debit amounts are displayed incolor-negative. - On list rows (home screen recent transactions, account transaction list): amounts are displayed in
color-text-primaryby 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-primaryunless the account is in debit, in which case the balance iscolor-negative.
Pending transactions¶
- Amount in
color-text-secondary(lighter than confirmed) - "Pending" status badge in
color-info - Amount shown in
text-bodyweight 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 intext-mono. - Full account number: available after biometric gate. Reveal with a tap-to-copy affordance.
- BSB (NZ) and routing codes (AU): displayed in
text-monowith space separator for readability:12-3456(BSB format) or062-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.. Never2.5%or2.5% per annum(use the abbreviation). - Never round an interest rate:
1.75% p.a.not1.8% p.a.or2% 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 USDwith fee shown asFee: $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-bodyand smaller. ≥ 3:1 fortext-h1,text-h2, andtext-display(≥ 18pt). - UI components (buttons, input borders, toggle tracks in their resting state) ≥ 3:1 against adjacent colour.
color-positiveandcolor-negativetext must meet 4.5:1 contrast oncolor-surfaceandcolor-surface-raisedin 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: nonewithout 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=noin 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:
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.
-
Every component that displays a financial amount must accept an explicit
currency: 'NZD' | 'AUD' | stringprop. The currency symbol, thousands separator, and decimal format must be derived from this prop using the sharedformatCurrencyutility. Never hardcode$or any currency symbol in a component. -
Never use
anyas 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;
}
- All components that fetch data must implement three render paths without exception:
- Loading: skeleton screen matching the approximate geometry of the loaded content. Never a spinner for list or card content.
- Loaded: the component renders correctly with valid data.
-
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. -
Colour must come from design tokens only. No hex values (
#1A1A2E), norgb(), nohsl(), 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. -
Every interactive element must have an accessible label that describes both the action and the context. Generic labels are defects:
- Correct:
aria-label="Freeze Everyday Account ending 4321" - Incorrect:
aria-label="Freeze" - Correct:
aria-label="View transaction: $45.00 at Countdown on Monday" -
Incorrect:
aria-label="View transaction" -
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 atsrc/utils/formatCurrency.ts. -
All forms must handle field-level validation errors returned from the API. The
errorsobject from the API response maps field names to error codes. These codes map to user-facing messages insrc/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 colourcolor-negative, error label visible) immediately on API error return, not only on re-submission. -
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.
-
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
BiometricGatecomponent. -
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.
-
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.
-
All list components that can contain more than 50 items must implement virtual rendering (windowed list). Use the project's standard virtual list component (
VirtualListfrom 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. -
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
- BSB (NZ):
-
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. -
The app must render correctly within iOS safe-area insets (notch, Dynamic Island, home indicator) and Android edge-to-edge mode. Use the
SafeAreaContextprovider fromreact-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.
Related¶
- 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