"Indian Ocean premium" — the mobile design system
A token-driven Flutter app: deep lagoon blues/teals as the brand identity, silver reserved exclusively for NAQD, dark-mode-first. This page documents the architecture and module map as verified directly in code, not from the design blueprint alone.
This page describes module structure, not an exhaustive screen inventory — treat specific screen lists as a snapshot, and the architecture/patterns as the durable part.
Architecture
WalletProvider is a registered app-wide ChangeNotifier; most other features call their service classes directly from local widget state.Design system
Palette
Named ramps, quoted directly from lib/core/theme/app_colors.dart. Dark is the canonical scheme — "designed first, light derived from it."
brand
brand dim
dark bg
light bg
NAQD only
USD identity
Per-currency identity is systematic: AppColors.forCurrency() maps MVR → lagoon, USD → ocean, NAQD → silver, everywhere in the app — a balance card, a transaction row, and an icon all agree on which color means which currency. The code comment on the silver ramp is explicit: "using it anywhere else dilutes the signal."
Typography
Inter for UI text, JetBrains Mono for addresses/hashes/reference IDs (both via google_fonts, network-fetched/cached — not bundled assets). Dedicated "money" text styles (moneyDisplay 40px down to moneySmall 13px) apply FontFeature.tabularFigures() and FontFeature.slashedZero() — the code's own reasoning: "so digits never jitter as a balance ticks... 0 and O are never confused in an account number."
Spacing, radius, motion
| Token set | Values |
|---|---|
| Spacing (4pt scale) | xxs=2 … giant=64, plus a Gap widget replacing bare SizedBox |
| Radius | xs=4 (badges) → pill=999, scaled so curvature reads consistently regardless of element size |
| Motion durations | fast=150ms (state flips), base=250ms (default — cards/sheets), slow=400ms (page transitions, balance count-ups), ambient=700ms (shimmer loops only) |
| Motion curves | standard=easeOutCubic, plus dedicated enter/exit/move/spring(easeOutBack)/emphasized curves |
Shared widget library (lib/core/widgets/)
19 widgets, each solving one duplicated problem the codebase's own comments call out — e.g. app_card.dart replaces hardcoded-white containers that were invisible in dark mode; status_badge.dart replaces four copy-pasted status-color switches; mynex_logo.dart replaces five hand-rolled logo copies with their own hardcoded blue. Notable ones: animated_money.dart (counts up on load), countdown_ring.dart (FX/partner quote expiry), empty_state.dart / error_state.dart (deliberately distinct — "empty means nothing here yet, error means we couldn't find out"), loading_skeleton.dart (content-shaped shimmer, not a spinner), pin_confirm_sheet.dart (bottom-sheet PIN confirmation for money actions).
Router & config
go_router — a flat List<GoRoute> with static path-constant helpers (e.g. WalletDetail.path(currency)); added specifically because, per the code comment, "thirteen context.push targets had no matching route." A custom errorBuilder shows a designed empty state instead of go_router's raw exception page.String.fromEnvironment('API_BASE_URL') in lib/core/config/app_config.dart. Debug builds fall back to an emulator-aware localhost; release builds throw if unset — no silent fallback to a dev machine ships in production.flutter run --dart-define=API_BASE_URL=http://localhost:8181/api--dart-define=API_CERT_SHA256=... activates the pinning interceptor — empty by defaultFeature module map
All under lib/features/<feature>/ with a {screens, widgets, services/providers, models} subfolder pattern (not every feature uses every subfolder).
| Module | Status | Notes |
|---|---|---|
splash | real | Startup gating — shows a blocked screen for misconfigured API_BASE_URL or a compromised device |
auth | real | Login, register, PIN setup/unlock, biometric setup — real API-backed |
home | real | Dashboard, consumes WalletProvider, skeleton/error-retry states wired |
wallet | real | Convert, wallet detail — the app's one globally-registered ChangeNotifier lives here |
payments | real | P2P transfer, QR pay/scan/generate, bill pay, request money — covers what the blueprint calls "P2P" and "QR" as one module, not two |
naqd | real | Buy/sell, operations list/detail, withdraw, silver price chart, reserve panel — async mint/burn with idempotency + polling wired end to end |
transactions | real | History + detail, reuses wallet's transaction model |
profile | real | Largest module — KYC submit/documents, 2FA, PIN change, sessions, login history, notification preferences. This is where "settings/security" from the blueprint actually lives; there is no separate settings/ directory |
cards | real | List/detail, activate, freeze, limits, card PIN, one-time PAN reveal, request physical — CardService calls the real /v1/cards/* routes, idempotency keys on every write |
partners | real | Rail listing, quote → execute → operation status for the TnG corridor — a deliberate three-call shape ("the rate is real money and moves") against /v1/partners/*, idempotency key on execute |
notifications | real | In-app notification list, mark-read/read-all against /v1/notifications/* |
The cards and partners modules were placeholder/absent early in
this build and landed as fully real, API-wired features in the course of writing this
documentation — a direct illustration of why this page describes structure rather than
promising a frozen screen inventory. By this snapshot, every screen category
REVAMP_BLUEPRINT.md §4 names checks out against real, API-wired code.
Security services (see also: Security page)
Full detail on Security → Mobile security. In short: biometric auth, 5-minute app auto-lock with a background privacy curtain, secure-storage-backed tokens (with a verified-removed legacy hardcoded-key AES layer), jailbreak/root detection blocking app start, and opt-in certificate pinning are all real, implemented code — not aspirational.
Testing
| File | Covers |
|---|---|
test_harness.dart | Shared helper — wraps widgets in the real AppTheme, disables network font fetches for deterministic tests |
amount_keypad_test.dart | AmountEntry digit entry/display logic |
qr_payload_test.dart | QR payload encode/decode, matched against the Go server's base64url format |
transaction_grouping_test.dart | Transaction list date-grouping logic |
transfer_review_test.dart | Widget test — fee/receipt rows against TransferBreakdown |
widget_test.dart | App-level smoke test with a mocked secure-storage channel |
No integration_test/ directory and no golden tests — all coverage is unit/widget level. Run with flutter test; static analysis with flutter analyze.