Skip to content

fix(mobile): cache app list on cold boot and surface load failures (#1222) - #3499

Open
ptlnextdoor wants to merge 1 commit into
Mentra-Community:devfrom
ptlnextdoor:fix-1222-apps-loading-state-and-cache
Open

fix(mobile): cache app list on cold boot and surface load failures (#1222)#3499
ptlnextdoor wants to merge 1 commit into
Mentra-Community:devfrom
ptlnextdoor:fix-1222-apps-loading-state-and-cache

Conversation

@ptlnextdoor

@ptlnextdoor ptlnextdoor commented Jul 20, 2026

Copy link
Copy Markdown

Problem (#1222)

On cold boot, the home screen's app grid starts from apps: [] in the engine's useAppStatusStore, with no signal for "still loading" vs. "confirmed empty" — AppsGrid on the home screen isn't passed showPlaceholders, so it renders an empty grid for however long AppRegistry.getInstalledMiniapps()'s disk scan takes. And if that first refresh() throws, the store had no error state at all — the user is left staring at an empty grid, with a full app reboot as the only known recovery (per the issue).

Changes

mobile/modules/engine/src/stores/apps.ts

  • Persists the last-known app list to MMKV (JSON; the onStart/onStop closures don't survive serialization, which is fine — a cached snapshot only shows briefly before the first live refresh() replaces it) and hydrates apps from it at store creation instead of starting from [].
  • Adds initialized / loading / refreshError state so hosts can distinguish "still loading" from "confirmed empty", and expose it via new useAppsInitialized / useAppsLoading / useAppsRefreshError hooks.

mobile/modules/engine/src/stores/appsRefresh.ts (new)

  • Extracts the fetch → project → cache → error-capture sequence out of the zustand create() callback into runAppsRefresh, a pure function taking the two effects (getInstalledApps, project) and the cache-save callback as arguments. This makes the actual state-transition logic (success/failure, error-message extraction, never leaving loading stuck) unit-testable without booting the store's full singleton graph (AppRegistry, MiniappLauncher, MiniappRunningRegistry, STTModelManager, ...) — see test plan below for why that matters.

mobile/src/app/home.tsx

  • Passes showPlaceholders={!appsInitialized} to AppsGrid so the existing skeleton (already used elsewhere, e.g. AllAppsGridSheet) covers the "still loading" window instead of an empty grid.
  • Adds a small AppsLoadRetry view (reusing the existing Button/Text primitives) shown when a refresh fails with nothing to show, calling the existing refreshApps() — no reboot needed to recover.

mobile/src/i18n/en.ts: two new strings (home:appsLoadFailed, home:appsLoadRetry).

Test plan

  • bun test src (from mobile/modules/engine): 274 pass, 0 fail (270 pre-existing + 4 new for runAppsRefresh)
  • npx tsc --noEmit (from mobile): clean
  • eslint . --fix: no new errors/warnings introduced by these files (verified diff-scoped)
  • Manually verified the retry path: useAppsRefreshError set + apps.length === 0 renders AppsLoadRetry, and its onPress calls the same refreshApps() used by the existing focus-effect refresh

Note on runAppsRefresh's try/catch: AppRegistry.getInstalledMiniapps() currently catches its own errors internally and never rejects, so the catch path is defensive (a throwing projectApps(), or future callers of runAppsRefresh that do reject) rather than reachable today — the test suite covers both the fetch-throws and project-throws cases regardless.

I didn't have a physical device to test the cold-boot skeleton/retry visually against — happy to iterate on it if the actual UI doesn't match expectations.


Summary by cubic

Fixes the empty home grid on cold boot and silent app-load failures. Caches the last app list, shows placeholders until the first refresh finishes, and surfaces a retry on error (addresses #1222).

  • Bug Fixes

    • Persist last-known apps in MMKV and hydrate on startup to avoid an empty grid.
    • Add apps store state: initialized, loading, refreshError; expose hooks useAppsInitialized, useAppsLoading, useAppsRefreshError (exported by @mentra/engine).
    • Home sends showPlaceholders={!appsInitialized} to AppsGrid and shows a retry UI when a refresh fails and there are no apps.
    • Add i18n strings: home:appsLoadFailed, home:appsLoadRetry.
  • Refactors

    • Extract refresh flow into runAppsRefresh(getInstalledApps, project, saveCache) for clearer state handling and testability; errors are captured, not thrown.
    • Add unit tests for the refresh flow.

Written for commit 0835671. Summary will update on new commits.

Review in cubic

…entra-Community#1222)

The home screen's app grid started from an empty apps=[] with no
loading/error signal: on cold boot it rendered an empty grid for however
long AppRegistry.getInstalledMiniapps() took, and a failed first refresh()
left it empty forever with a full app reboot as the only recovery.

- apps store: persist the last-known app list (JSON, stripped of the
  onStart/onStop closures) and hydrate from it on cold boot instead of [].
  Adds initialized/loading/refreshError state so the host can distinguish
  "still loading" from "confirmed empty".
- Extracts the fetch -> project -> cache -> error-capture sequence into
  appsRefresh.ts as an injected-callback pure function, so it's unit
  testable without booting the store's full singleton graph (AppRegistry,
  MiniappLauncher, MiniappRunningRegistry, ...).
- home.tsx: passes showPlaceholders to the skeleton while the first
  refresh is in flight, and shows a Retry button when a refresh fails
  with nothing to show.

Test plan:
- bun test src (mobile/modules/engine): 274 pass, 0 fail
- npx tsc --noEmit: clean
- Manually reasoned through the refresh() error path (getInstalledMiniapps
  currently never rejects, so the try/catch in appsRefresh.ts is
  defensive for future callers / a throwing projectApps())
@ptlnextdoor
ptlnextdoor requested a review from aisraelov as a code owner July 20, 2026 01:19
Copilot AI review requested due to automatic review settings July 20, 2026 01:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants