feat(mobile): Task 9 — core infrastructure (config, logger, supabase, query client, auth store) - #110
Conversation
…lient, auth store Task 9 (#12). Every screen from Task 10 onward depends on this layer; none of it renders anything itself. - src/lib/config.ts — reads the EXPO_PUBLIC_* values from the manifest and throws at import naming any that are missing. Turns issue #95's silent `undefined` endpoint into a startup error. - app.config.js — new: Task 7 shipped a static app.json, so nothing lifted those variables into `extra`. Spreads app.json rather than replacing it. - src/lib/logger.ts — MobileLogger, the only permitted console caller; no-console is now an error everywhere else. Data argument is Record<string, unknown> so a Session or Error cannot be passed whole. - src/lib/supabase.ts — expo-secure-store adapter, detectSessionInUrl: false. - src/lib/queryClient.ts — MMKV-persisted query cache, AppState focus and NetInfo online listeners registered once at import. - src/store/authStore.ts, src/constants/{colors,thresholds}.ts Deviations from REQ-17, each recorded in the plan with its reason: react-native-mmkv v4 renamed the v3 API the spec used; the sync persister the spec named is deprecated in favour of the async one; APP_ENV is EXPO_PUBLIC_APP_ENV and an unrecognised value falls back to production, not development. Coverage: 100% on all seven new files. Closes #12
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…refresh wiring
sanitize() redacted only top-level keys, so the guarantee was depth-dependent:
{ access_token } was redacted while { context: { access_token } } logged the
token verbatim at warn, a level production still emits. Task 9 is what made
this load-bearing — it routes every mobile log through one gateway whose doc
comment sells sanitize() as the second defensive layer — so the fix lands in
packages/logging and applies to apps/web and apps/api too. It now recurses
through plain objects and arrays and guards cycles with a WeakSet. Class
instances are still passed through untouched; that stays the LogData type's
job, and the limitation is recorded rather than left implied.
MobileLogger gains warnWithCause/errorWithCause, which CLAUDE.md > Logging
Strategy §6 requires for Supabase-auth and JWT errors. Without them Task 10's
auth paths had no compliant way to log a cause at all, since LogData
deliberately rejects a raw Error. Its level gate now compares through the
LEVELS table @pulseticker/logging already exports, as apps/web does, instead
of a boolean per method.
supabase.ts wires startAutoRefresh/stopAutoRefresh to AppState. auth-js drives
the refresh from a 30s setInterval and React Native suspends JS timers while
backgrounded, so autoRefreshToken alone left a resumed app issuing its next
request with an expired JWT. The secure-store adapter now logs and rethrows
instead of passing rejections through silently, where a refused write meant an
unexplained sign-out on the next cold launch.
authStore makes clearSession the single teardown path — setSession(null)
delegates to it, so sign-out cleanup added later cannot be skipped by the
onAuthStateChange caller.
The Config fixture duplicated across four test suites moves to one helper, and
the userInterfaceStyle note in colors.ts is corrected: the pin applies on iOS
only, because expo-system-ui is not installed (#113).
Filed rather than fixed here: #112 (single-source the web/mobile palette),
#113 (expo-system-ui, needs a device build), #114 (packages/logging has no
test project, so sanitize() has no measured coverage).
pnpm build 6/6, pnpm test 6/6, mobile 59 tests, 100% on all seven src files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfHB8Q3zSQ5idotURDwkyv
Review round — 9 findings, 6 fixed here, 3 filedReviewed the diff for correctness and quality. Five further candidates were raised and refuted on verification, recorded at the bottom so they do not get re-litigated. Fixed in b1b8c25
Also: the Filed rather than fixed
Refuted on verificationRecorded so they are not raised again: the VerificationStill not verified on a device — unchanged from the original PR, and #95 still gates that. The |
…undled versions (#119) The first native build of apps/mobile failed to compile. One error: ExpoWorkletsBridgeProvider.mm:236 no member named 'executeSync' in 'worklets::WorkletRuntime' expo/bundledNativeModules.json fixes react-native-worklets at 0.10.1 and react-native-reanimated at 4.5.1, but neither is a direct dependency — expo-router declares reanimated as a bare `*` peer and nothing pins worklets — so pnpm resolved the newest satisfying versions, 0.12.1 and 4.6.0. expo-modules-core is written against the <=0.10 worklets API, and 0.12 removed executeSync. Its peer range states the constraint correctly, but the peer is optional, so pnpm installs the mismatch with a warning rather than an error. The pins go in pnpm-workspace.yaml overrides, not only in apps/mobile's dependencies. Declaring them in the app is necessary — it should say what it links — but not sufficient: expo-modules-autolinking resolves native modules out of the store, and @expo/ui and expo-router still bound the 0.12.x copy. With the override the tree holds 0.10.1 and 4.5.1 alone and pnpm peers check is clean. Nothing in CI caught this because nothing in CI compiles native code — jest, tsc and eslint all passed throughout. That gap is filed as #118. Verified by building and running on the iOS simulator: ExpoWorkletsBridgeProvider.mm compiles, Build Succeeded, the app installs, Metro bundles 1347 modules, and the Task 7 smoke screen reports both workspace packages resolved — which also exercises the sanitize() rewrite from #110 at runtime. pnpm build 6/6, pnpm test 6/6. Closes #117 Claude-Session: https://claude.ai/code/session_01NfHB8Q3zSQ5idotURDwkyv Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Task 9 of REQ-17 — the shared non-UI layer every screen from Task 10 onward depends on. Nothing here renders.
Closes #12
What landed
app.config.jsEXPO_PUBLIC_*intoexpo.extra. Task 7 shipped a staticapp.json, so nothing did this beforesrc/lib/config.tssrc/lib/logger.tsMobileLogger— the only permitted console caller;no-consoleis now an error everywhere elsesrc/lib/supabase.tsdetectSessionInUrl: falsesrc/lib/queryClient.tssrc/store/authStore.tssession,setSession,clearSession, nothing elsesrc/constants/{colors,thresholds}.tsSTALE_WARNING_MS/STALE_DISCONNECTED_MSconfig.tsis what turns #95 (EAS cloud builds have no source for threeEXPO_PUBLIC_*variables) from a silentundefinedendpoint into a launch-time error naming the variable. #95 still has to supply the values; this makes its absence loud.Deviations from REQ-17 — all four recorded in the plan with reasons
react-native-mmkvv4 renamed the v3 API the spec used.new MMKV({id})→createMMKV({id}),delete(key)→remove(key). The v3 calls do not exist on a v4 instance, so the spec's adapter would have thrown on the first cache eviction, not at construction. A test assertsremove()specifically.createSyncStoragePersistercarries an@deprecatedtag in@tanstack/query-sync-storage-persister@5.102, pointing atcreateAsyncStoragePersister. Switched. MMKV's synchronous methods satisfy the async persister'sAsyncStorageinterface unchanged (every field isMaybePromise), andPersistQueryClientProviderrestores asynchronously either way — the hydration behaviour REQ-17 describes is unaffected.EXPO_PUBLIC_APP_ENV, notAPP_ENV—eas.jsonand.env.example, both merged in Task 7, already settled on that name.appEnvfalls back toproduction, notdevelopment. CLAUDE.md > Logging Strategy fixes the undefined environment at the quietest level; defaulting the other way means a build that simply forgot the variable ships with debug logging on.Issue #12's body also said
gcTime: 300_000andsrc/lib/query-client.ts/src/store/auth.ts. The plan is followed instead:gcTimeis 24h (a query evicted after 5 minutes is never written to MMKV, which would put the skeleton state on every launch, not just the first), and the filenames arequeryClient.ts/authStore.tsper the folder structure in REQ-17.Two further changes made while implementing:
react-native-nitro-modulesadded as a direct dependency. It is a required peer ofreact-native-mmkv@4and was not resolvable fromapps/mobileunder pnpm's isolated layout. Same rule as Hoisted pnpm store mixes jest 29.x and 30.x — name-resolved config can bind the wrong major #97: every consumer declares what it resolves by name.userInterfaceStylepinned to"light".colors.tsships a light palette only — REQ-17 Phase 1 specifies no dark palette, and a second one would be invented rather than designed — so"automatic"would pair dark system chrome with light screens.Test boundary
__tests__/{config,logger,supabase,queryClient,authStore,constants}.test.ts— 43 tests, 100% on all seven new files.Mocked:
expo-constants,expo-secure-store,@supabase/supabase-js,react-native-mmkv,@react-native-community/netinfo. Per CLAUDE.md, each mock is asserted to have actually intercepted (createClientcall count, captured MMKV/NetInfo/AppState handlers) rather than only that behaviour looked right.sanitize()runs for real — the redaction assertions would pass against a no-op mock.What the tests pin down, beyond the happy path:
config.tstreats an empty string as missing (an unset EAS variable arrives that way) and names only the variables actually absentlogger.tsredacts atwarnanderrortoo — the levels production still emitsqueryClient.tsgcTime >= persister.maxAge; norefetchIntervalin the defaults;isConnected: null(NetInfo before its first probe) is not read as online; the focus listener's cleanup actually unsubscribesVerification
expo config --type publicwas run with and without the variables set, confirmingapp.jsonis merged andextrais populated — the change is not inferred from the config file alone.Not verified: nothing here has run on a device. The MMKV native module and the secure-store adapter need an EAS development build, which needs #95 first.
🤖 Generated with Claude Code