Skip to content

fix(mobile): settle profile and invoice reads on a flaky connection - #8265

Open
iscekic wants to merge 2 commits into
mainfrom
drain/mobile-settle-profile-invoice-reads
Open

iscekic wants to merge 2 commits into
mainfrom
drain/mobile-settle-profile-invoice-reads

Conversation

@iscekic

@iscekic iscekic commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

What

The Profile tab's credits balance and Linked accounts reads, and the organization invoices first page, ran with TanStack Query's default network mode. A request that started while the app reported offline stayed paused: pending without ever fetching. The credits slot and the Linked accounts section then had no state to leave, and the invoices screen kept its skeleton rows with no error or retry to escape them.

  • apps/mobile/src/components/profile-credits-card.tsx: the balance and credit-block reads run with networkMode: 'always' and an explicit refetchOnReconnect: true. @tanstack/query-core derives refetchOnReconnect = networkMode !== 'always' when it is undefined, so the reconnect refetch has to be asked for.
  • apps/mobile/src/lib/hooks/use-current-user-id.ts: the identity read settles the same way, so an unresolved getMe can no longer leave the credits read disabled behind it. The paused branch in isUnreachable is now unreachable and removed.
  • apps/mobile/src/components/profile-screen.tsx: the providers read settles the same way, and both failure surfaces on the screen present the offline error variant when the connectivity probe has committed offline. The choice is reactive, so a connectivity commit re-renders it.
  • apps/mobile/src/lib/hooks/use-organization-queries.ts: buildOrgInvoicesPageQueryOptions settles its first page the same way.
  • apps/mobile/src/lib/organization-invoice-error-variant.ts: the invoices error-variant rule moved into a pure selector, unit-tested, so the screen stays inside its line budget. A permanent code wins over connectivity; a confirmed-offline failure presents connectivity; unknown keeps the neutral retryable copy.

Two consequences worth stating. A read now interrupts the wait: a refused connection fails in about a second instead of waiting for the app to be told it is online again, and a hanging one is bounded by the existing tRPC control-plane deadline (15 s) plus the app's default retry budget (1 s + 2 s), so the worst case to a visible error is about 48 s, as on every other screen. The reconnect refetch means recovery no longer needs a manual Retry.

Verification

Local stack on this VM (Postgres, Next.js, Metro), Android emulator, local fake account with a personal context and one seeded organization. No production data or endpoints involved.

Mounted tests against a real query client with the online manager committed offline. Each behavior test fails without the change:

  • profile-credits-card.mounted.test.tsx: the balance renders while the app reports offline, and an unreachable balance settles to the failure copy with no skeleton.
  • profile-screen.queries.mounted.test.tsx: the linked account renders while offline; an unreachable read settles to Could not load accounts with Retry; a confirmed-offline failure presents the offline variant.
  • use-organization-queries.test.ts: the invoices options settle to an error while offline and refetch once connectivity returns.
  • organization-invoice-error-variant.test.ts: the variant precedence.
  • Full suite: pnpm --filter kilo-app test — 1195 files, 17907 tests passed.

On the emulator, observed states:

  1. Online: credits $0.00 with Add credits, and the Linked accounts row with the account address.
  2. Next.js stopped: credits "Failed to load balance. Tap to retry." with Add credits, Linked accounts "Could not load accounts" with Retry, invoices "Something went wrong" with Retry. No skeleton on any of the three.
  3. Radio disabled with the offline banner up, Retry pressed: the reads still run and fail, so the surfaces switch to the offline copy ("Failed to load").
  4. Radio and Next.js restored: credits, the linked account row, and the invoices state recover without a manual Retry.

Limitation: this environment has no Stripe-backed organization invoices, so the invoices screen settles to its retryable error rather than rows or the empty state. The rows and empty branches are covered by the existing mounted suite.

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: Identity read: How should the credits read stop depending on a possibly-paused user.getMe identity read?
    Answer: Settle getMe app-wide
  2. Question: Error wording: How should a read that settles by failing report itself?
    Answer: Pick the offline variant reactively
  3. Question: Retry budget: Worst-case time to a visible error is bounded by the existing retry budget. Keep it or tighten it for these reads?
    Answer: Keep the app's default retry

The Profile credits card, the Linked accounts row, and the invoices first page
ran their reads with TanStack Query's default network mode, so a request that
started while the app reported offline paused: it stayed pending without ever
fetching. The credits slot and the linked-accounts section kept their skeleton
placeholders, and the invoices list held its skeleton rows with no error or
retry to leave them.

Those reads now run with no network-mode gate and an explicit reconnect
refetch, so each settles on content or on a retryable error and recovers when
connectivity returns. The identity read settles the same way, so an unresolved
getMe can no longer keep the credits read disabled. A read that fails while the
device is confirmed offline now presents the offline error variant.
@iscekic iscekic added drain Work item from the findings drain explorer:android Finding surfaced by the Android explorer labels Oct 20, 2026
Comment thread apps/mobile/src/lib/hooks/use-current-user-id.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The follow-up commit restores the offline cold-start fast path for the app-wide identity read while keeping the settling behavior, and its test/mocking updates are consistent; no new issues found in the incremental diff.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Verification

  • apps/mobile/src/lib/hooks/use-current-user-id.ts:58 now reports isUnreachable when onlineManager reports offline (data === undefined && !refused && (!managerOnline || isFetched)), so _layout.tsx:254's offlineUserId resolves the restored account immediately on an offline launch instead of waiting for the attempt to fail. With the manager online, the condition is unchanged, so the prior behavior is preserved.
  • The subscription is useSyncExternalStore-based and unsubscribes on unmount (no listener leak); the snapshot is a boolean, so it is stable for React.
  • Terminal refusals still win: refused keeps isUnreachable false even while offline, so a rejected credential still settles on the error surface.
  • The only consumer of isUnreachable is apps/mobile/src/app/_layout.tsx; other callers read userId/isLoading/isError/isFetchError, which are untouched.
  • Tests updated: the direct hook suite pins the offline fast path, and the mounted suites that partially mock @tanstack/react-query now spread the original module so the new onlineManager import resolves.
  • The invoices observer type annotation only removes the fixed unknown page type in a test; the assertions under test (isError, isPending, refetch counts) are unchanged.
Files Reviewed (4 files)
  • apps/mobile/src/lib/hooks/use-current-user-id.ts - 0 issues
  • apps/mobile/src/lib/hooks/use-current-user-id.test.ts - 0 issues
  • apps/mobile/src/lib/hooks/use-organization-queries.test.ts - 0 issues
  • apps/mobile/src/lib/hooks/use-restored-account-id.mounted.test.tsx - 0 issues

Note: verification was limited to static review; no test suite was executed in this read-only session.

Previous Review Summary (commit 1ca48b7)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 1ca48b7)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The TanStack networkMode: 'always' change removes the paused fast path from the app-wide identity read (useCurrentUserId), which can stall the offline cold-start bootstrap on the loading surface for up to the 15 s control-plane deadline.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/hooks/use-current-user-id.ts 47 App-wide identity gate loses its offline fast path: with networkMode: 'always' and the paused branch removed, isUnreachable only flips after an attempt fails, so _layout.tsx's offlineUserId recovery waits for the getMe attempt (up to the 15 s deadline) and holds the bootstrap loading surface on a genuinely offline cold start.
Files Reviewed (14 files)
  • apps/mobile/src/components/organization/invoices-screen.mounted.test.tsx - 0 issues
  • apps/mobile/src/components/organization/invoices-screen.tsx - 0 issues
  • apps/mobile/src/components/profile-credits-card.mounted.test.tsx - 0 issues
  • apps/mobile/src/components/profile-credits-card.tsx - 0 issues
  • apps/mobile/src/components/profile-screen.queries.mounted.test.tsx - 0 issues
  • apps/mobile/src/components/profile-screen.signout.mounted.test.tsx - 0 issues
  • apps/mobile/src/components/profile-screen.test-helpers.tsx - 0 issues
  • apps/mobile/src/components/profile-screen.tsx - 0 issues
  • apps/mobile/src/lib/hooks/use-current-user-id.test.ts - 0 issues
  • apps/mobile/src/lib/hooks/use-current-user-id.ts - 1 issue
  • apps/mobile/src/lib/hooks/use-organization-queries.test.ts - 0 issues
  • apps/mobile/src/lib/hooks/use-organization-queries.ts - 0 issues
  • apps/mobile/src/lib/organization-invoice-error-variant.test.ts - 0 issues
  • apps/mobile/src/lib/organization-invoice-error-variant.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 83.8K · Output: 14.7K · Cached: 1.8M

Review guidance: REVIEW.md from base branch main

@iscekic

iscekic commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Verified on the Android emulator against the local dev stack (local fake account, personal and seeded organization contexts). Each capture was read back and matches the state named in its alt text.

  • Credits and the Linked accounts row with the backend reachable.
  • Credits and Linked accounts failure surfaces with Next.js stopped: a retryable error on both, no skeleton.
  • Linked accounts with the radio disabled and the offline banner up: the read still runs and fails, so the surface presents the offline copy.
  • Both recovered after the radio and the backend returned, with no manual retry.
  • Invoices: the settled error state, its offline copy, and recovery after reconnect.

Limitation: this environment has no Stripe-backed organization invoices, so the invoices screen settles to its retryable error rather than rows or the empty state. Those branches are covered by the mounted suite.

Credits balance and purchase action rendered with the backend reachable

Linked accounts row rendered with the backend reachable

Credits card showing its retryable failure copy with the backend stopped

Linked accounts showing its retryable failure copy with the backend stopped

Linked accounts presenting the offline copy while the device reports no connection

Credits balance restored after connectivity and the backend returned

Invoices screen showing its settled retryable error state

Invoices screen presenting the offline copy while the device reports no connection

Invoices screen back on its server error state after reconnect

The identity read now starts regardless of the online manager, so it is no
longer paused while the app reports offline. That removed the immediate
signal the root layout used to open on the restored account on a cold start:
the bootstrap would wait for the attempt to fail instead, up to the
control-plane deadline on a hanging connection.

The hook subscribes to the online manager and reports the identity as
unreachable as soon as the manager reports offline, so an offline launch still
resolves the restored account at once while the attempt settles on its own.

Also annotate the invoices observer in its test so the assertion follows the
router's page types instead of a fixed one.
@iscekic iscekic added the merge:human A human reviews and merges this pull request. label Oct 20, 2026
@iscekic

iscekic commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Re-ran the device check on head 95e8532 with the local stack (Postgres, Next.js, Metro) and a local fake account with a seeded organization. The device reported offline (airplane mode) and the API was unreachable (the tunnel to the API port was removed); each capture was read back and matches the state named.

  • Cold start with the device offline and the API unreachable opened on the app's Home surface with the offline banner, not a loading surface, and the Profile tab resolved the restored account.
  • Profile credits settled to its retryable failure copy, with no skeleton.
  • Linked accounts settled to its error state with Retry.
  • Organization invoices settled to the offline copy with Retry and no skeleton rows.
  • With connectivity and the API restored, the credit balance and the linked account row returned without a manual Retry, and the invoices read refetched on its own.

Limitation: the local environment has no Stripe-backed invoices, so the invoices read answers with a server error rather than rows or the empty state; those branches stay covered by the mounted suite.

Credits card settled to its retryable failure copy while the device reported offline

Linked accounts settled to its error state with Retry while the device reported offline

Invoices screen settled to the offline copy with Retry

Offline cold start opening on the app surface with the offline banner

Credit balance restored after connectivity returned, without a manual retry

This branch has not been deployed

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

Labels

drain Work item from the findings drain explorer:android Finding surfaced by the Android explorer merge:human A human reviews and merges this pull request.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant