Mobile App · Surface walkthrough

Domain detail (mobile — polymorphic domain route)

A per-surface walkthrough of the Mobile App mobile surface: layout, states, interactions, data, and cross-references.

drafted
8sections4 minread

On this page

Context. surface mobile · domain shell-routing · route /domain/[domainId] · auth anon (entry) | gated per-domain entitlement tier (paywall on lock) · source apps/oshun/mobile/app/domain/[domainId].tsx

Last walked. not yet walked

Purpose#

/domain/[domainId] is the single polymorphic domain screen. It resolves domainId (tara, veritas, nyx, arete, nisaba, metis), evaluates entitlement access, and renders a domain-specific entry hero plus the full domain content (continuity, claims, sky events, check-ins, passages, etc.) inside one ScrollView. It also handles startup loading, access-locked, offline-cache, temporarily-unavailable, planned-preview, and deep-link-focus states. At ~14k lines it is by far the largest mobile screen.

Entry points#

  • buildDomainRouteHref(domainId, {...}) from Home, Explore, Activity, Library, Profile (and inter-domain switches). Params: origin, stack, path, ref, unavailable, reason, offline, plus per-domain hydration query.
  • Notification routing / deep links resolving to a domain path.
  • Stack screen domain/[domainId] (title "Domain", headerBackTitle "Back", gesture-enabled) declared in _layout.tsx.

Layout regions#

  • Header: stack header titled "Domain" with Back; gesture back enabled.
  • Hero / above-the-fold: domain-specific DomainEntryHero (eyebrow, title, body, stateLabel, metaItems, primary + secondary actions) — variants per domain (mobile-domain-entry-hero-<domain>).
  • Main: mobile-domain-screen-<domain> ScrollView — badge, title, route intro body, route-context chips, planned-preview card (Metis), deep-link-focus card, E2E harnesses (Tara, flagged), then the domain-specific content blocks.
  • Alternate full-screen states: loading skeleton; access-locked; offline fallback (Tara/Veritas/Nisaba cached lists or generic recovery panel); unavailable recovery panel + domain switcher.

States#

  • LoadingstartupLoading renders skeleton badge/title/body/cards + "Loading module...".</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Empty — per-domain empty handling inside content blocks (e.g. no saved objects); hero still leads with a primary action."/> <strong>Empty</strong> — per-domain empty handling inside content blocks (e.g. no saved objects); hero still leads with a primary action.</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Populated (short/long) — full domain content; long scroll."/> <strong>Populated (short/long)</strong> — full domain content; long scroll.</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Error (recoverable) — domainUnavailable → mobile-domain-unavailable-recovery-panel with Retry domain launch + "Try another domain" switcher (filtered to allowed/available domains)."/> <strong>Error (recoverable)</strong> — <code>domainUnavailable</code> → <code>mobile-domain-unavailable-recovery-panel</code> with Retry domain launch + "Try another domain" switcher (filtered to allowed/available domains).</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Error (unrecoverable) — caught by shell error boundary; unknown domainId falls through to unknown testIDs."/> <strong>Error (unrecoverable)</strong> — caught by shell error boundary; unknown <code>domainId</code> falls through to <code>unknown</code> testIDs.</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Offline — offlineMode (≠ nyx) → cached Tara meditations / Veritas briefings / Nisaba passages+notes lists, each launchable from cache; otherwise generic mobile-domain-offline-<domain> recovery panel. Nyx has its own offline sky-map path."/> <strong>Offline</strong> — <code>offlineMode</code> (≠ nyx) → cached Tara meditations / Veritas briefings / Nisaba passages+notes lists, each launchable from cache; otherwise generic <code>mobile-domain-offline-<domain></code> recovery panel. Nyx has its own offline sky-map path.</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Permissions-denied — N/A at this screen (capture is on Home)."/> <strong>Permissions-denied</strong> — N/A at this screen (capture is on Home).</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Gated — domainAccess.allowed === false → " access locked" with current/required tier, reason, Upgrade plan (→ Profile paywall), Back, and an available-domains switcher."/> <strong>Gated</strong> — <code>domainAccess.allowed === false</code> → "<title> access locked" with current/required tier, reason, <strong>Upgrade plan</strong> (→ Profile paywall), Back, and an available-domains switcher.</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: iOS vs Android — Nyx night-mode palette; otherwise dark theme; back gesture enabled (fullScreenGestureEnabled)."/> <strong>iOS vs Android</strong> — Nyx night-mode palette; otherwise dark theme; back gesture enabled (<code>fullScreenGestureEnabled</code>).</li> </ul> <h2 id="interactions" class="hd hd-l2">Interactions<a class="hd-anchor" href="#interactions" aria-label="Permalink to this section">#</a></h2> <ul> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Domain entry hero — primary action (mobile-domain-entry-primary-<domain>)"/> <strong>Domain entry hero — primary action</strong> (<code>mobile-domain-entry-primary-<domain></code>) <ul> <li>Function: per-domain (Tara: launch/resume meditation or course / write reflection; Veritas: resume reading; Nyx: resume observation; Arete: start check-in / view streaks; Nisaba: open passage). Pushes domain path or starts an in-screen session.</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Domain entry hero — secondary actions (...-secondary-<domain>-*)"/> <strong>Domain entry hero — secondary actions</strong> (<code>...-secondary-<domain>-*</code>) <ul> <li>Function: open trending brief / top claim / saved brief (Veritas); sky map / highlight / log (Nyx); top goal / journal prompt (Arete); workspace / passage (Nisaba); etc.</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Upgrade plan (access-locked, Pressable)"/> <strong>Upgrade plan</strong> (access-locked, Pressable) <ul> <li>Function: <code>openUpgradeFlow(requiredTier)</code> → push Profile w/ <code>intent=upgrade</code>, <code>domain</code>, <code>requiredTier</code> params.</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Back to (access-locked / offline / unavailable)"/> <strong>Back to <previous></strong> (access-locked / offline / unavailable) <ul> <li>Function: <code>handleBackInShell</code> → <code>router.replace</code> previous domain or origin tab; haptic.</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Domain switch chip (switchChip, access-locked + unavailable states)"/> <strong>Domain switch chip</strong> (<code>switchChip</code>, access-locked + unavailable states) <ul> <li>Function: <code>switchDomain(domain)</code> (only allowed + available domains shown).</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Retry domain launch (offline/unavailable recovery panels)"/> <strong>Retry domain launch</strong> (offline/unavailable recovery panels) <ul> <li>Function: <code>retryDomainLaunch</code>.</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Offline cached card (Tara/Veritas: ContinuityCard; Nisaba: PassageListItem/NotebookCard)"/> <strong>Offline cached card</strong> (Tara/Veritas: <code>ContinuityCard</code>; Nisaba: <code>PassageListItem</code>/<code>NotebookCard</code>) <ul> <li>Function: set offline route path, exit offline mode, launch from cache / open cached brief/passage/note; haptic + notice. testIDs <code><domain>-offline-cache-offline-card/passage/note-<id></code>.</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Deep-link focus CTA (mobile-domain-deep-link-cta-<domain>)"/> <strong>Deep-link focus CTA</strong> (<code>mobile-domain-deep-link-cta-<domain></code>) <ul> <li>Function: <code>handleOpenDeepLinkFocus</code> (opens the resolved deep-link target).</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: E2E Tara complete session (tara-e2e-complete-session, flagged)"/> <strong>E2E Tara complete session</strong> (<code>tara-e2e-complete-session</code>, flagged) <ul> <li>Function: <code>handleCompleteTaraSessionForE2E</code> (forces session completion for Maestro). Also <code>TaraReflectionCaptureCard</code> (note input, suggestions, save).</li> </ul> </li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Per-domain content controls — extensive: Tara quick-reset/sleep-timer selectors + audio session; Veritas save/follow article+claim, topic follow; Nyx save/remind/night-mode/sky-map; Arete goals/habits/journal/balance/routine; Nisaba annotations (note input, color, visibility, save/edit), compare, grounded evidence. (Each block has its own testIDs; document per-domain in a follow-up pass if needed.)"/> <strong>Per-domain content controls</strong> — extensive: Tara quick-reset/sleep-timer selectors + audio session; Veritas save/follow article+claim, topic follow; Nyx save/remind/night-mode/sky-map; Arete goals/habits/journal/balance/routine; Nisaba annotations (note input, color, visibility, save/edit), compare, grounded evidence. (Each block has its own testIDs; document per-domain in a follow-up pass if needed.)</li> </ul> <h2 id="data--contracts" class="hd hd-l2">Data & contracts<a class="hd-anchor" href="#data--contracts" aria-label="Permalink to this section">#</a></h2> <ul> <li><strong>Reads</strong>: <code>@oshun/domain-registry</code> (<code>getDomainMetadata</code>, <code>isKnownDomain</code>, offline-fallback + launch-permission), per-domain libs (<code>@oshun/domain-tara| veritas|nyx|arete|nisaba</code>, <code>@oshun/shell-routines</code>), BFF client (Nisaba passage/compare/grounded-evidence/manuscript/annotations), offline caches, auth + profile + offline-queue context, runtime config.</li> <li><strong>Writes</strong>: BFF annotation save; library/topic/object follow + save state; Tara reflection records; offline-mode toggles.</li> <li><strong>Realtime</strong>: shared audio session manager (<code>getSharedAudioSessionManager</code>).</li> <li><strong>Caching</strong>: per-domain offline caches; in-memory fixtures (<code>TARA_CONTINUE_SESSION</code>, <code>VERITAS_*</code>, <code>NYX_*</code>, <code>ARETE_*</code>, <code>NISABA_*</code>).</li> <li><strong>Auth/role check</strong>: <code>evaluateDomainAccess(domain, entitlements)</code> → allowed/required tier; Metis surfaces enforce tier before navigation.</li> </ul> <h2 id="cross-references" class="hd hd-l2">Cross-references<a class="hd-anchor" href="#cross-references" aria-label="Permalink to this section">#</a></h2> <ul> <li>Entry builders: <code>@/navigation/routes</code> (<code>buildDomainRouteHref</code>, <code>buildTabRouteHref</code>); context parse <code>parseDomainRouteContext</code>.</li> <li>Upgrade target: profile.md (<code>intent=upgrade</code> paywall).</li> <li>Component sources: <code>@/components/MobileRecoveryPanel</code>, <code>DomainEntryHero</code>, <code>ContinuityCard</code>, <code>PassageListItem</code>, <code>NotebookCard</code>, <code>TaraReflectionCaptureCard</code>.</li> </ul> <h2 id="open-questions--known-gaps" class="hd hd-l2">Open questions / known gaps<a class="hd-anchor" href="#open-questions--known-gaps" aria-label="Permalink to this section">#</a></h2> <ul> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Per-domain content blocks are extensive (each domain has dozens of controls) — this file documents the shared scaffold + hero + state machine; a per-domain deep walk should follow."/> Per-domain content blocks are extensive (each domain has dozens of controls) — this file documents the shared scaffold + hero + state machine; a per-domain deep walk should follow.</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Many domain datasets are in-module fixtures alongside live BFF reads (notably Nisaba) — confirm which are live in production."/> Many domain datasets are in-module fixtures alongside live BFF reads (notably Nisaba) — confirm which are live in production.</li> <li class="task"><input class="task-cb" type="checkbox" disabled="" aria-label="Incomplete: Nyx is excluded from the generic offline/unavailable branches (has its own night/offline-sky handling) — verify Nyx offline parity."/> Nyx is excluded from the generic offline/unavailable branches (has its own night/offline-sky handling) — verify Nyx offline parity.</li> </ul> </article> <footer class="doc-foot"> <span>Mobile App · Surface walkthrough</span> <span><a href="../vdocs-index.html">Documentation portal</a> · <a href="domain-detail.md">Source</a></span> </footer> </main> <aside class="toc-rail" aria-label="On this page"> <div class="toc-head">On this page<span class="toc-count"></span></div> <div class="tocfilter-wrap"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"> <circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg> <input id="tocFilter" type="text" placeholder="Filter sections" autocomplete="off" aria-label="Filter sections"/><!-- stub:legitimate: standard HTML placeholder attribute --> </div> <nav><ul class="toc"><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#purpose" class="toc-link">Purpose</a></div></li><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#entry-points" class="toc-link">Entry points</a></div></li><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#layout-regions" class="toc-link">Layout regions</a></div></li><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#states" class="toc-link">States</a></div></li><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#interactions" class="toc-link">Interactions</a></div></li><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#data--contracts" class="toc-link">Data & contracts</a></div></li><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#cross-references" class="toc-link">Cross-references</a></div></li><li class="toc-h2"><div class="toc-row"><span class="toc-bullet"></span><a href="#open-questions--known-gaps" class="toc-link">Open questions / known gaps</a></div></li></ul></nav> </aside> </div> </div> </div> <button id="backtop" aria-label="Back to top" aria-hidden="true" tabindex="-1"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" aria-hidden="true"> <path d="M12 19V5M5 12l7-7 7 7"/></svg> </button> <div class="cmdk" id="cmdk" aria-hidden="true"> <div class="cmdk-backdrop" data-cmdk-close></div> <div class="cmdk-panel" role="dialog" aria-modal="true" aria-label="Search documentation"> <div class="cmdk-inputwrap"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"> <circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg> <input id="cmdkInput" type="text" autocomplete="off" spellcheck="false" role="combobox" aria-expanded="false" aria-controls="cmdkResults" aria-autocomplete="list" aria-haspopup="listbox" placeholder="Search the documentation center…" aria-label="Search query"/><!-- stub:legitimate: standard HTML placeholder attribute --> <kbd>esc</kbd> </div> <div class="cmdk-facets" id="cmdkFacets" role="group" aria-label="Filter results by type, audience, layer, and version"></div> <div class="cmdk-results" id="cmdkResults" role="listbox"></div> <div class="cmdk-foot"> <span><kbd>↑</kbd><kbd>↓</kbd> navigate</span> <span><kbd>↵</kbd> open</span> <span><kbd>esc</kbd> close</span> <span class="cmdk-foot-sp"></span> <span id="cmdkCount"></span> </div> </div> </div> <div class="kmodal" id="kmodal" role="dialog" aria-modal="true" aria-label="Keyboard shortcuts" aria-hidden="true"> <div class="kmodal-backdrop"></div> <div class="kmodal-panel"> <div class="kmodal-head"> <b>Keyboard shortcuts</b> <button class="icon-btn" id="kmClose" aria-label="Close keyboard shortcuts"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"> <path d="M18 6L6 18M6 6l12 12"/></svg> </button> </div> <dl class="kmodal-list"> <div class="kmodal-row"><dt>Search all docs</dt><dd><kbd>/</kbd><kbd>⌘K</kbd></dd></div> <div class="kmodal-row"><dt>Move through results</dt><dd><kbd>↑</kbd><kbd>↓</kbd> then <kbd>Enter</kbd></dd></div> <div class="kmodal-row"><dt>Toggle left navigation</dt><dd><kbd>[</kbd></dd></div> <div class="kmodal-row"><dt>Toggle on-page contents</dt><dd><kbd>]</kbd></dd></div> <div class="kmodal-row"><dt>Close overlays</dt><dd><kbd>Esc</kbd></dd></div> <div class="kmodal-row"><dt>This panel</dt><dd><kbd>?</kbd></dd></div> </dl> </div> </div> <div class="mmodal" id="mmodal" role="dialog" aria-modal="true" aria-label="Expanded diagram viewer" aria-hidden="true"> <div class="mmodal-bar"> <b>Diagram — scroll to zoom, drag to pan</b> <div class="mmodal-tools"> <button id="mmZoomOut" title="Zoom out" aria-label="Zoom out">−</button> <button id="mmReset" title="Reset" aria-label="Reset zoom" style="font-size:13px;">1:1</button> <button id="mmZoomIn" title="Zoom in" aria-label="Zoom in">+</button> <button id="mmClose" title="Close" aria-label="Close diagram">×</button> </div> </div> <div id="mstage"></div> <div class="mmodal-hint">Press Esc to close</div> </div> <script src="../docs-center/assets/reader.js"></script> </body> </html>