This document is the governance baseline for the six core UI states — Loading, Empty, Error, Success, Disabled, Pending — across PocketPay Mobile's primary screens: Wallet, Send, Receive, Transactions (Activity), Contacts, Vault, Settings, and Diagnostics.
Use this as a required review reference when building or changing a screen or reusable component. A row marked not applicable still needs a reason; it must not be omitted accidentally. Existing screens that do not yet meet a requirement should be treated as implementation gaps, not as alternative patterns. For full step-by-step journeys (e.g. the multi-step create/import wallet flow), see Main wallet user flows. For vault-specific Testnet/mock-mode language, see Vault UI Guidance. For visual tokens (colours, spacing, the shared Button/Input components), see the Design System guide. All states must also pass the Mobile Accessibility Audit Checklist.
State conventions used throughout the app:
- Loading — data or code needed to render the next stable state is being fetched or calculated. Keep existing usable data visible during refresh where possible, expose a busy state, and provide progress text for waits longer than a moment.
- Empty — the request succeeded but there is no content. Explain why the state is valid and provide a safe next action where one exists. Never present an empty result as an error.
- Error — the requested operation did not complete. Preserve safe user input and previously loaded data, use actionable copy, and provide retry or correction near the error.
- Success — an operation completed or valid data is available. Confirm what changed without relying on colour alone, then move to the next stable state.
- Disabled — an action is intentionally unavailable. Make the control visibly and semantically disabled and explain non-obvious prerequisites; never use disabled styling as the only busy signal.
- Pending — the app accepted an action but the result is not final. Name the phase (
Reviewing,Signing,Submitting,Waiting for confirmation) and prevent duplicate execution. Pending may contain a loading indicator, but is distinct from initial data loading because user intent has already been accepted.
When more than one state could apply, render the most actionable state: a blocking error takes priority over empty, pending takes priority over disabled styling for the action in progress, and stale content should remain visible beneath non-blocking refresh or error feedback.
Route: app/(tabs)/index.tsx · Store: useWalletStore (publicKey, balance, transactions, isLoading, refreshWalletData)
| State | Behavior |
|---|---|
| Loading | Pull-to-refresh RefreshControl is active while balance and the 3-item recent activity preview load together. |
| Empty | A valid, non-error state: unfunded wallet shows 0.0000000 XLM and No recent transactions — not treated as an error. |
| Error | A failed refresh stops loading and leaves the screen in a recoverable state (data preserved, user can pull to refresh again); if Horizon is unavailable, loading stops without clearing existing data. |
| Success | Populated balance, abbreviated public key, and the 3 most recent operations (sent shown with a minus sign, received with a plus sign). |
| Disabled | Not applicable — Home has no primary action to disable; the header/tab remains interactive during loading. |
| Pending | While a send/receive-related action is being prepared elsewhere in the flow, keep the wallet visible and avoid implying the balance is final if the transaction has not been confirmed yet. |
Routes: app/send.tsx, app/review-transaction.tsx · Stores: useWalletStore, useSignerStore
| State | Behavior |
|---|---|
| Loading | The review action shows a spinner while the transaction is signed and submitted; wallet refresh after completion must not make the confirmed result appear to regress. |
| Empty | Not applicable — the form intentionally renders with empty/default input values on entry. Empty fields are a form condition, not an empty-screen state. |
| Error | Local validation (missing/invalid destination or amount, amount ≤ 0, amount exceeds balance) is shown beside the field before review. A signing or submission failure shows safe, actionable copy and preserves the editable values for correction or retry. |
| Success | The completion state identifies the amount and recipient, refreshes wallet data, and offers an unambiguous path back to Wallet or Activity. |
| Disabled | Review Transaction is disabled until required fields are valid. Confirm/submit is disabled during signing or submission and when signing prerequisites are unavailable. |
| Pending | Use explicit phases for Reviewing → Signing → Submitting → Waiting for confirmation. The accepted payment cannot be submitted twice, and navigation away from an irreversible phase must be guarded. |
Route: app/receive.tsx · Store: useWalletStore (publicKey)
| State | Behavior |
|---|---|
| Loading | Not applicable — the QR code and public key render immediately from local wallet state; no network fetch. |
| Empty | No public key found is shown if the public key is unavailable; Copy Address / Share must not be invoked with an empty value. |
| Error | A copy/share platform failure is shown with recoverable feedback while the address remains visible. A missing public key uses the empty state above, not a generic error. |
| Success | QR code, full public key (selectable text), Copy Address, and Share (opens OS share sheet with title "My Stellar Address") are all available. |
| Disabled | Copy/Share actions are visibly and semantically disabled when no public key is present. |
| Pending | No long-running pending state is expected; if share/copy is in progress, keep the UI responsive and return to the steady receive view immediately after the OS action is triggered. |
Route: app/(tabs)/history.tsx · Store: useWalletStore (transactions, isLoading, refreshWalletData, publicKey)
| State | Behavior |
|---|---|
| Loading | RefreshControl indicator is visible while the full transaction list (up to 20 recent Horizon operations) loads. |
| Empty | No transactions found with explanatory copy — a valid state for a new/unfunded wallet. |
| Error | Refresh failure ends loading, the list remains usable with previously-loaded data, and another pull-to-refresh can be attempted. |
| Success | Full list renders newest-first, with sent/received direction, amount, and localized timestamp per row. |
| Disabled | Not applicable — list has no primary action to disable. |
| Pending | During an in-flight transfer, show the new transaction as pending or keep the list stable with a clear "pending confirmation" indicator until Horizon confirms it. |
Route: app/contacts.tsx · Store: useAppStore (contacts, addContact, removeContact)
| State | Behavior |
|---|---|
| Loading | Scanning: full-screen QrScanner camera view with a scan-window overlay and Close action (functions as this screen's loading/in-progress state). |
| Empty | No contacts yet, with + Add Manually and Scan QR entry points. |
| Error | Invalid/duplicate address or missing name shows inline error (manual form) or an Alert (scan flow); entered values remain editable. Delete is gated behind a destructive confirmation alert to prevent accidental removal. |
| Success | Contact list populated with name + abbreviated public key per row; after a QR scan, the address field is pre-filled and read-only under Save Scanned Contact. |
| Disabled | Address field becomes read-only after a successful scan (user can only edit the name before saving). |
| Pending | While scanning or saving a scanned contact, show that the device is working and prevent duplicate submissions. |
Note: contacts scanning has a debounce guard (hasScanned + lastScanTime, SCAN_DEBOUNCE_MS = 1.5s) so a single physical QR code can't fire multiple scan events — see user-flows.md for detail and the AC11 test group in __tests__/contacts.scan.test.tsx.
Route: app/(tabs)/vault.tsx · Store: useWalletStore (publicKey, getSecretKey) / vaultStore (locks, lockedBalance, unlockTime)
| State | Behavior |
|---|---|
| Loading | VaultLockList shows a loading state while fetching/calculating lock status; balance fetch shows loading via mockFetchVaultBalance (mock mode) or Soroban simulation (real mode). |
| Empty | VaultLockList has a dedicated empty state when the user has no locks yet. |
| Error | Real-mode balance/deposit/withdraw calls that fail (e.g. RPC unavailable) surface an error — the app currently has no RPC-failure fallback, so this should be treated as an area needing careful, honest error copy rather than a silent failure. Contract/Soroban result codes are not yet mapped to user-friendly messages — mock functions currently throw generic errors only. |
| Success | Vault balance, TESTNET badge, and the locks list (each with locked amount, unlock date, status, and eligible actions for matured locks) render correctly. |
| Disabled | "Lock Funds (30 days)" and withdraw actions must clearly indicate mock mode when no real contract is configured (EXPO_PUBLIC_VAULT_CONTRACT_ID unset) — this isn't a literal disabled button, but the UI must not imply a live/production action is being taken. Matured vs. immature locks must be visually distinct so users can tell which locks are eligible for withdrawal. |
| Pending | Vault deposit, withdraw, and lock actions should show a clear in-flight state while the transaction is being prepared or confirmed, especially in mock mode where the outcome is simulated. |
| Unavailable | When no wallet is connected, the vault feature is disabled via configuration (EXPO_PUBLIC_VAULT_ENABLED=false), or the SDK reports the vault backend as not ready, the interactive form is replaced by a VaultUnavailableState card showing why the vault cannot be used and offering fallback navigation (to settings or retry). See src/utils/vaultAvailability.ts for evaluation logic. |
Testnet & custody language: every vault state must follow Vault UI Guidance — no "savings account"/"bank"/"insured"/"secured" language, and mock mode must be visually distinguished from real contract mode at all times, per that doc's Summary Checklist.
Route: app/(tabs)/settings.tsx · Stores/hooks: useAppStore, useAppLockStore, useWalletStore, useNetworkEnvironment
| State | Behavior |
|---|---|
| Loading | Secret-key access, app-lock authentication, and wallet reset show a labelled busy state on the initiating control or confirmation modal. The rest of the settings screen remains readable when safe. |
| Empty | A missing wallet renders WalletEmptyState with create/import actions instead of wallet-specific settings. An environment field with no configured value uses explicit Not configured copy rather than a blank row. |
| Error | Secure-storage, authentication, or reset failures use non-sensitive, actionable messages. Environment warnings remain beside the affected configuration and do not rely on badge colour alone. |
| Success | The selected theme/lock state and environment values are updated in place. Secret-key reveal succeeds only after explicit user action and remains clearly marked as sensitive. |
| Disabled | Destructive confirmation is disabled while reset is running. Controls whose prerequisites are unavailable expose a reason; development-only feature flags are omitted from production rather than rendered as an unexplained disabled row. |
| Pending | Lock enable/disable, authentication, and reset use one in-flight owner, ignore duplicate taps, and do not show the final toggle or signed-out state until persistence succeeds. |
Route: app/diagnostics.tsx · Utility: getDiagnostics
| State | Behavior |
|---|---|
| Loading | Initial load and manual refresh announce Loading diagnostics, set the content busy, and keep the last successful snapshot visible during refresh. |
| Empty | No recent error, no enabled feature flags, or no configured wallet is shown as an explicit valid value such as None recorded this session or No wallet configured, never as a blank section. |
| Error | Collection or JSON parsing failure shows a redacted inline error and a labelled Retry action. Previously collected diagnostics remain available if they are safe to display/export. |
| Success | Redacted environment, network, feature-flag, storage, wallet, and last-failure values are readable; export shares exactly the redacted snapshot described in Diagnostics. |
| Disabled | Export is disabled until a valid snapshot exists. Refresh is disabled while already refreshing. The synthetic error action is omitted outside development builds. |
| Pending | Refresh and export identify the active operation, prevent duplicate taps, and return focus to the initiating control or updated heading when the operation completes. |
Reusable components own state behavior that would otherwise be reimplemented inconsistently. A screen may add context, but must not weaken these contracts.
| Component or pattern | Required state contract |
|---|---|
Button / AsyncActionButton |
Disable immediately when an async action starts, show stable loading text, expose disabled and busy, ignore duplicate presses, and restore the enabled state after failure. |
Input / FormField |
Keep a persistent visible label, place validation feedback beside the field, preserve the entered value after recoverable errors, and expose read-only/disabled state semantically. |
LoadingState / EmptyState / unavailable-state cards |
State what is happening, occupy a predictable layout, and offer a labelled retry or next action when one exists. Empty and unavailable are not interchangeable with error. |
| Error and status banners | Use safe, actionable copy; pair colour with text/icon; announce meaningful changes; and remain visible long enough to act on. |
TransactionListItem / StatusBadge |
Present direction, amount, asset, counterparty, time, and pending/confirmed/failed status as one understandable summary. Unknown status must not be guessed. |
ConfirmModal / review-confirm patterns |
Name the consequence, provide cancel and confirm paths, trap accessibility focus while open, make destructive intent explicit, and lock both dismissal and confirmation while the action is pending. |
QrScanner and QR display |
Provide permission, scanning, invalid-code, and close states; debounce scan results; and always provide a non-camera/non-visual alternative. |
- Does a component derive visual copy, interaction blocking, and accessibility state from one source of truth?
- Can two rapid taps start the action twice, including before React re-renders?
- What does the user see after a rejected promise, lost network connection, empty result, or unmount during completion?
- Does the component preserve safe content and focus across loading, retry, modal open/close, and success?
- Are state differences communicated with text or semantics in addition to colour and animation?
- Do focused tests query controls by role/label and cover default, disabled, pending, success, and error behavior that the component owns?
- Editing / disabled: keep Review Transaction disabled until destination and amount are valid; validation errors remain beside their fields.
- Review / pending: present recipient, amount, asset, memo, fee, network, and signer as one reviewable summary. No funds have been sent yet.
- Signing and submitting / pending + loading: change the phase copy, mark the confirm action busy/disabled, guard navigation, and prevent a second submission.
- Confirmed / success: show a durable receipt, then reconcile Wallet and Activity without hiding the receipt behind a refresh spinner.
- Rejected / error: explain whether the user should edit, retry, or wait; retain safe form values and never display raw SDK or secret material.
- Resolve unavailable, mock, or configured mode before enabling the action.
- Review the amount, unlock conditions, network, custody limitations, and mode before confirmation.
- During preparation/submission/confirmation, show the named pending phase and disable conflicting deposit/withdraw actions.
- On success, update the lock list and balance together. On failure, keep the prior snapshot and show vault-specific recovery guidance.
- Keep the previous redacted snapshot visible while refresh is pending and disable duplicate refresh.
- Replace the snapshot only after collection and parsing succeed; otherwise retain it with an inline retryable error.
- Enable export only for a valid redacted snapshot, identify the OS share-sheet transition, and do not add secrets or raw exception payloads.
Other routes (app/(auth)/* onboarding, app/scan.tsx, app/payment-success.tsx, app/transaction/*) have their state behavior documented step-by-step in user-flows.md. They still follow the state definitions and reusable-component contracts above.