diff --git a/apps/mobile/src/MobileApp.tsx b/apps/mobile/src/MobileApp.tsx
index c8939ef3..f3144531 100644
--- a/apps/mobile/src/MobileApp.tsx
+++ b/apps/mobile/src/MobileApp.tsx
@@ -83,7 +83,7 @@ export function MobileApp() {
setCompactNavigation((state) => selectLoadedNote(state, notes, selectedNoteId))
}, [])
- useMobileVaultRuntimeLoader({
+ const runtimeLoader = useMobileVaultRuntimeLoader({
appStateStorage,
loadNotes: loadDemoVaultNotes,
metadataStorage: vaultMetadataStorage,
@@ -127,9 +127,11 @@ export function MobileApp() {
createNoteTitle={createFlow.title}
isCreatePromptOpen={createFlow.isPromptOpen}
isCreatingNote={createFlow.isCreating}
+ runtimeLoadFailed={runtimeLoader.failed}
onCancelCreateNote={createFlow.cancel}
onChangeCreateNoteTitle={createFlow.setTitle}
onOpenCreateNote={createFlow.open}
+ onRetryRuntimeLoad={runtimeLoader.retry}
onSubmitCreateNote={createFlow.submit}
onSelectNote={selectNote}
/>
@@ -155,9 +157,11 @@ export function MobileApp() {
createNoteTitle={createFlow.title}
isCreatePromptOpen={createFlow.isPromptOpen}
isCreatingNote={createFlow.isCreating}
+ runtimeLoadFailed={runtimeLoader.failed}
onCancelCreateNote={createFlow.cancel}
onChangeCreateNoteTitle={createFlow.setTitle}
onOpenCreateNote={createFlow.open}
+ onRetryRuntimeLoad={runtimeLoader.retry}
onSubmitCreateNote={createFlow.submit}
onSelectNote={selectNote}
/>
@@ -179,9 +183,11 @@ function CompactShell({
createNoteTitle,
isCreatePromptOpen,
isCreatingNote,
+ runtimeLoadFailed,
onCancelCreateNote,
onChangeCreateNoteTitle,
onOpenCreateNote,
+ onRetryRuntimeLoad,
onSubmitCreateNote,
onSelectNote,
selectedNoteId,
@@ -194,12 +200,14 @@ function CompactShell({
createNoteTitle: string
isCreatePromptOpen: boolean
isCreatingNote: boolean
+ runtimeLoadFailed: boolean
onNavigate: (event: CompactNavigationEvent) => void
onDeleteNote?: () => void
onDraftChange: (draft: MobileEditorDraft) => void
onCancelCreateNote: () => void
onChangeCreateNoteTitle: (title: string) => void
onOpenCreateNote: () => void
+ onRetryRuntimeLoad: () => void
onSubmitCreateNote: () => void
onSelectNote: (note: MobileNote) => void
selectedNoteId: string
@@ -244,10 +252,12 @@ function CompactShell({
createNoteTitle={createNoteTitle}
isCreatePromptOpen={isCreatePromptOpen}
isCreatingNote={isCreatingNote}
+ runtimeLoadFailed={runtimeLoadFailed}
onCancelCreateNote={onCancelCreateNote}
onChangeCreateNoteTitle={onChangeCreateNoteTitle}
onOpenCreateNote={onOpenCreateNote}
onOpenSidebar={() => onNavigate({ type: 'openSidebar' })}
+ onRetryRuntimeLoad={onRetryRuntimeLoad}
onSelectNote={onSelectNote}
onSubmitCreateNote={onSubmitCreateNote}
/>
@@ -294,10 +304,12 @@ function NoteListPanel({
createNoteTitle,
isCreatePromptOpen,
isCreatingNote,
+ runtimeLoadFailed,
onCancelCreateNote,
onChangeCreateNoteTitle,
onOpenCreateNote,
onOpenSidebar,
+ onRetryRuntimeLoad,
onSelectNote,
onSubmitCreateNote,
selectedNoteId,
@@ -307,10 +319,12 @@ function NoteListPanel({
createNoteTitle: string
isCreatePromptOpen: boolean
isCreatingNote: boolean
+ runtimeLoadFailed: boolean
onCancelCreateNote: () => void
onChangeCreateNoteTitle: (title: string) => void
onOpenCreateNote: () => void
onOpenSidebar?: () => void
+ onRetryRuntimeLoad: () => void
onSelectNote: (note: MobileNote) => void
onSubmitCreateNote: () => void
selectedNoteId: string
@@ -323,6 +337,7 @@ function NoteListPanel({
} />
+ {runtimeLoadFailed ? : null}
item.id}
@@ -377,6 +392,17 @@ function NoteListPanel({
)
}
+function VaultLoadErrorNotice({ onRetry }: { onRetry: () => void }) {
+ return (
+
+ Could not load vault notes.
+ [styles.vaultLoadRetry, pressed ? styles.pressed : null]}>
+ Retry
+
+
+ )
+}
+
function selectLoadedNote(
state: ReturnType,
loadedNotes: MobileNote[],
diff --git a/apps/mobile/src/styles.ts b/apps/mobile/src/styles.ts
index 89963889..55bc67cb 100644
--- a/apps/mobile/src/styles.ts
+++ b/apps/mobile/src/styles.ts
@@ -5,6 +5,7 @@ import { noteCreateStyles } from './styles/noteCreateStyles'
import { noteListStyles } from './styles/noteListStyles'
import { propertiesStyles } from './styles/propertiesStyles'
import { sidebarStyles } from './styles/sidebarStyles'
+import { vaultLoadStyles } from './styles/vaultLoadStyles'
export const styles = {
...commonStyles,
@@ -14,4 +15,5 @@ export const styles = {
...noteListStyles,
...propertiesStyles,
...sidebarStyles,
+ ...vaultLoadStyles,
}
diff --git a/apps/mobile/src/styles/vaultLoadStyles.ts b/apps/mobile/src/styles/vaultLoadStyles.ts
new file mode 100644
index 00000000..07b2e448
--- /dev/null
+++ b/apps/mobile/src/styles/vaultLoadStyles.ts
@@ -0,0 +1,32 @@
+import { StyleSheet } from 'react-native'
+import { colors, spacing } from '../theme'
+
+export const vaultLoadStyles = StyleSheet.create({
+ vaultLoadError: {
+ marginHorizontal: spacing.lg,
+ marginBottom: spacing.sm,
+ padding: spacing.md,
+ borderColor: colors.chipRed,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderRadius: 8,
+ backgroundColor: '#fff8f7',
+ },
+ vaultLoadErrorText: {
+ color: colors.text,
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ vaultLoadRetry: {
+ alignSelf: 'flex-start',
+ marginTop: spacing.sm,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.xs,
+ borderRadius: 6,
+ backgroundColor: colors.primarySoft,
+ },
+ vaultLoadRetryText: {
+ color: colors.primary,
+ fontSize: 13,
+ fontWeight: '700',
+ },
+})
diff --git a/apps/mobile/src/useMobileVaultRuntimeLoader.ts b/apps/mobile/src/useMobileVaultRuntimeLoader.ts
index 3fe62f27..6de5927c 100644
--- a/apps/mobile/src/useMobileVaultRuntimeLoader.ts
+++ b/apps/mobile/src/useMobileVaultRuntimeLoader.ts
@@ -1,4 +1,4 @@
-import { useEffect } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import type { MobileAppStateStorage } from './mobileAppStateStorage'
import type { MobileNote } from './demoData'
import type { MobileVaultMetadata } from './mobileVaultMetadata'
@@ -20,19 +20,42 @@ export function useMobileVaultRuntimeLoader({
selectedNoteId: string | null
}) => void
}) {
+ const [loadState, setLoadState] = useState<'loading' | 'ready' | 'failed'>('loading')
+ const [reloadKey, setReloadKey] = useState(0)
+
+ const retry = useCallback(() => {
+ setLoadState('loading')
+ setReloadKey((key) => key + 1)
+ }, [])
+
useEffect(() => {
let isActive = true
void loadMobileVaultRuntime({ appStateStorage, loadNotes, metadataStorage })
.then((runtime) => {
- if (isActive && runtime.notes.length > 0) {
+ if (!isActive) {
+ return
+ }
+
+ if (runtime.notes.length > 0) {
onLoaded(runtime)
}
+ setLoadState('ready')
+ })
+ .catch(() => {
+ if (isActive) {
+ setLoadState('failed')
+ }
})
- .catch(() => {})
return () => {
isActive = false
}
- }, [appStateStorage, loadNotes, metadataStorage, onLoaded])
+ }, [appStateStorage, loadNotes, metadataStorage, onLoaded, reloadKey])
+
+ return {
+ failed: loadState === 'failed',
+ isLoading: loadState === 'loading',
+ retry,
+ }
}
diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md
index 6d457f72..8f0d06da 100644
--- a/docs/MOBILE_PROGRESS.md
+++ b/docs/MOBILE_PROGRESS.md
@@ -72,6 +72,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
- Replaced the demo vault's hardcoded identity with the shared default vault metadata boundary.
- Wired mobile runtime loading through the persisted vault metadata catalog boundary, so app state, note loading, autosave, note creation, and deletion all operate against the active vault metadata.
- Extracted runtime loading into a hook plus tested pure loader to keep `MobileApp.tsx` at CodeScene `10.0`.
+- Added first-class runtime vault load failure state with a visible retry notice in the note list, replacing the previous silent failure path.
## Next Action
@@ -79,7 +80,7 @@ Continue Phase 3 with app-managed vault storage hardening:
1. Add a focused simulator interaction path for create/open/edit/autosave/delete once Expo Go's overlay no longer blocks clean screenshots.
2. Decide whether archive should be modeled as a first-class note state or deferred until the mobile vault schema exists.
-3. Add first-class error/retry state for vault runtime load failures instead of silently falling back to fixture notes.
+3. Add a small runtime empty-vault state once user-created blank vaults exist.
## Verification Log
@@ -248,6 +249,11 @@ Continue Phase 3 with app-managed vault storage hardening:
- CodeScene after runtime vault metadata wiring: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/mobileDemoVault.ts`, `apps/mobile/src/mobileVaultRuntime.ts`, and `apps/mobile/src/mobileVaultRuntime.test.ts` scored `10`; `apps/mobile/src/useMobileVaultRuntimeLoader.ts` returned no scorable code and no findings.
- `pnpm --filter @tolaria/mobile test` passed after runtime vault metadata wiring: 22 files / 70 tests.
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after runtime vault metadata wiring.
+- `pnpm --filter @tolaria/mobile test -- src/mobileVaultRuntime.test.ts` passed after runtime load retry UI: 22 files / 70 tests.
+- `pnpm --filter @tolaria/mobile typecheck` passed after runtime load retry UI.
+- CodeScene after runtime load retry UI: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/styles/noteListStyles.ts`, and `apps/mobile/src/styles/vaultLoadStyles.ts` scored `10`; `apps/mobile/src/useMobileVaultRuntimeLoader.ts` returned no scorable code and no findings.
+- `pnpm --filter @tolaria/mobile test` passed after runtime load retry UI: 22 files / 70 tests.
+- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after runtime load retry UI.
## Risks / Watch Items